Showing posts with label tdd. Show all posts
Showing posts with label tdd. Show all posts

Monday, September 25, 2023

Teaching TDD... but what just happened?

In my CS222 class, I spend a lot of time talking about, demonstrating, and giving feedback on Test-Driven Development (TDD). Indeed, from the very first day of class, we're doing TDD. For several years, I've opened the course by talking about confidence and how we can take small steps to maintain it. Last week was the fifth week of the semester, and my students had just started the "two-week project." That project integrates the concepts from the first four weeks of the semester into a single deliverable that of course includes TDD.

On Thursday, I was showing them how we can write a custom parser for data pulled from Wikipedia. I started by writing a test that, given my sample data, my as-of-yet unwritten parser could extract the piece that I want. Then, we wrote a clearly naive implementation of the parser that just returned that value as a literal. The literal has no place in the specification, and so our refactoring moved us to a correct solution. I made a big deal about how we had just completed a red-green-refactor sequence, summarizing each step to them as I have just done for you above. I especially emphasized that at the time of writing the test, we didn't care at all how we would make it work.

A student pushed back a little on the solution, asking why we didn't just go back and change the test case itself to something else. I responded that this would not be right, because before we wrote any production code, we first had agreed that this case was correct--and it was. I reminded them again about red-green-refactor. At this, a different student raised his hand to comment. These are not his exact words, but his observation was essentially this: "Are you saying, then, that at the red step, we write a test that tells us what we need next, but we don't worry about how it will work; then we make it work however we can; and then we clean it up to make it work right?"

I paused for a moment and then agreed, telling him that I really couldn't have said it better myself, thinking to myself as I said this that I had, indeed, said basically that same thing, many times in the past five weeks. At this, several heads in the class nodded sagely, making expressions as if they had gained some new insight into the process.

On one hand, I am happy that they seem to understand it now better than they did before, that this students' summarizing the process seemed to resonate. Yet I am plagued by the question, What just happened? What was actually different about this explanation than before?

I will see the students again tomorrow, and I'm thinking about just asking them. I don't want to make anyone feel badly about not understanding it before, but I do want to know if they can identify what tipped them toward understanding it now.

Saturday, May 13, 2023

Awaiting multiple signals in Godot GDScript

I ran into a problem with my summertime project where I want to wait for multiple coroutines or signals to complete. These happen to be related to animations. Depending on the situation, multiple different animations may play together, but I don't want to advance until all of them are complete. I have used some other asynchronous programming libraries that have built-in support for this, but alas, Godot has no such abstraction. 

I built this utility class that fills the bill. As with my previous post, I decided to share the code here since the "live" version is locked away in a private repository.

class_name CompositeSignal
extends Node

signal finished

var _remaining : int

func add_signal(sig: Signal):
    _remaining += 1
    await sig
    _remaining -= 1
    if _remaining == 0:
        finished.emit()
    

func add_call(object, method_name:String, argv:Array=[])->void:
    _remaining += 1
    await object.callv(method_name, argv)
    _remaining -= 1
    if _remaining == 0:
        finished.emit()

This was developed using TDD using GUT. Here's the test code. There's not actually a test here to demonstrate that the two add_ techniques work together, but that is exactly how my integration works in practice.

extends GutTest

## The amount of time a timer can wait that is safe for a unit test to run
const _TIMER_DURATION := 0.1

var _executions := 0

func before_each()->void:
    _executions = 0


func test_add_call(p=use_parameters([[1],[3]])):
    var composite = autofree(CompositeSignal.new())
    var number_to_add :int = p[0]
    for i in number_to_add:
        composite.add_call(self, "_run_coroutine")
    await wait_for_signal(composite.finished, 2, "Signal should have returned")
    assert_eq(_executions, number_to_add, \
        "There should have been %d invocations." % number_to_add)


func _run_coroutine()->void:
    await get_tree().create_timer(_TIMER_DURATION).timeout
    _executions+=1


func test_add_signal():
    var generator :SignalGenerator = autofree(SignalGenerator.new(self))
    
    var composite = autofree(CompositeSignal.new())
    composite.add_signal(generator.a_signal)
    generator.run()
    
    await wait_for_signal(composite.finished, 2, "Signal should have fired by now.")
    assert_true(generator.completed, "Expected to wait for generator state to update")
    
    
    
class SignalGenerator:
    signal a_signal
    
    var context
    var completed := false
    
    func _init(context_p):
        context = context_p
    
    func run():
        await context.get_tree().create_timer(_TIMER_DURATION).timeout
        completed = true
        a_signal.emit()

Wednesday, May 10, 2023

Running Godot unit tests using GUT with a pre-commit hook

I've been tinkering with a project idea for months, creating and discarding over a dozen variations. I was hoping to have a prototype or vertical slice complete before the start of summer, but I was unable to do so. This current variation is promising, but I ran into a situation where a lot of the code I had laid down was hard to test. In particular, I needed to check the interaction of subsystems that were probabilistic, and that's not only difficult to do manually, it's also prone to regression. Time for unit tests!

I'm using Godot Engine 4.0, and so I brought in GUT and started redesigning my code to be more testable. This alone is almost certainly worth the effort. Tests lose their value if they are not executed, and so I started looking into automatic ways to run my tests. Godot Engine has no such system built in, but I am using git, and so I investigated pre-commit hooks. This is something I've known about, and I think I may have even tinkered with before, but I don't believe I have ever incorporated them into a project. My previous work with automation used IDEs with built-in capability to run tests on commit, or I used server-side integration tests to check my work.

Someone who does a lot of shell scripting and git hooks would probably laugh at how simple this is. However, I want to share the script here in part because the "live" version is currently hidden away in a private repository. Here is the executable script that I put into my project's .git/hooks directory:

#!/bin/sh
if godot -d -s --path project addons/gut/gut_cmdln.gd -gdir=res://test/ -gprefix="" -gsuffix="_test.gd" -gexit
then
    echo "Tests passed"
else
    cat <<\EOF
Unit tests failed. Commit aborted.
EOF
    exit 1
fi

This works pretty well. One downside is that it has to pop up a window during the tests. I would like to find a way to prevent that. Also, I could probably get away with having the tests run only when I'm committing to master rather than on feature branches.