11

I'm trying to practice BDD by applying it to a simple problem—in this case, the flocking algorithm, better known as "boids".

Before any of the rules (cohesion, alignment, etc.) comes the most fundamental behavior: movement. The rules mutate a boid's velocity, but that means nothing unless the velocity mutates the boid's position.

Should I define an acceptance test for it?

Feature: Movement
    Scenario: Updating position according to velocity.
        Given a boid,
        When it moves,
        Then its new position should be the sum of its old position and its velocity.

On one hand, this is a critical piece of functionality without which the rest of the software falls apart. On the other, the logic is dead simple vector addition, and the test code would be equivalent to the code it's testing.

// The test:
assert_equal(old_position + velocity, new_position)

// The code under test:
self.position += self.velocity

Is there any point to such a test?

4 Answers 4

12

Yes, test the vital but simple behavior. At least for 3 reasons:

  1. The whole idea behind BDD is to promote collaboration between business and technical people, to achieve a common understanding, and make sure that what is delivered corresponds to the expectations. Making shortcuts because it's technically simple does not help business people to get confidence that acceptance criteria are really met.
  2. Taking bad habits might lead business users not to express cases which they think are simple, since you let them understand that simple could not be important to check.
  3. It may look simple at the beginning, but as the system gets iteratively more complex, you're never protected against unexpected side effects, for example when the velocity is no longer constant in time (acceleration, deceleration).

But your example is flawed. BDD is not just TDD expressed in plain text. Scenarios are not be used for the functional decomposition of a feature or algorithm specification. They are meant to describe behavior from a user perspective. It should therefore not depend on the internals of your implementation (e.g. scenario description should stay valid even if angular velocity is used instead of cartesian velocity).

Your formula is by the way only correct if the velocity is constant and expressed for a unit of time for which the motion is checked. Maybe this should be the scenario title, and another scenario would be for time-dependent velocity (acceleration/deceleration).

4
  • 2
    Note also that OP's code contains a bug: It assumes the framerate is constant, which might not be valid if the system is under heavy load. The correct calculation must be time-aware, unless you are using a real-time OS. Commented Dec 28, 2023 at 23:16
  • 1
    "Scenarios... are meant to describe behavior from a user perspective." I've been ruminating on this for the past few days, and I realized I can't distinguish between the behavior and its implementation. For example, take the rule of cohesion, which states that a boid should fly toward the center of its neighbors. What does "fly" mean? Do I create separate variables for velocity & acceleration, or do I directly increase the velocity? It feels like I need some way of knowing how I'm going to implement a feature to know how to test it. Commented Jan 2, 2024 at 9:08
  • 1
    To give context, I figured that by starting with movement, a feature that core features—the rules—depend on but not a core feature in itself, I had gone against BDD's outside-in philosophy, so I changed course and started with the rules; to begin with, cohesion. But I don't know what I should be testing. That the boid has moved x distance in y time? Factors such as speed are variable. I don't know how to test the "try" part; the effect of a rule. Commented Jan 2, 2024 at 9:34
  • 1
    Even the "when" step is unclear. When 1 second has passed? 1 millisecond? Ideally, I'd say, "In the next step of the simulation," but as everyone's pointed out, this is not a discrete simulation, like Conway's Game of Life. I need to provide a time delta. Commented Jan 2, 2024 at 9:37
8

One reason I don't use BDD on the regular is that the devil is in the details, and BDD hides detail rather than exposing it. For example, your last line is

Then its new position should be the sum of its old position and its velocity.

I'm instantly confused:

  • Are the boids moving in one-, two-, three-, or some higher-dimensional space? Or is the code meant to handle any positive number of dimensions?
  • Does adding position and velocity make sense? In physics, position is defined in terms of metres, and velocity is defined using metres per second, so they can't be added as physical properties. Which leads to the next confusion:
  • What about the time step? If you're trying to simulate boids live your calculation needs to take into account how long it's been since the last move. You wouldn't want the boids to behave differently when viewed with different refresh rates, or on different CPUs.

There is a more fundamental problem with your test code, if I read it correctly. Compare with the following test:

old_position = (1, 2, 3)
velocity = (0, 1, 2)
new_position = (1, 3, 5)  # This is key!
boid = Boid(old_position, velocity)
boid.move()
assert_equal(new_position, boid.position)

Now, instead of repeating the calculation from your production code in the test, we're actually checking that the calculation does the right thing, and that the thing it does is easily verifiable. Verifying that + does the same thing in your test and the production code is pointless, but verifying that + in fact adds up the relevant parts of the vector is quite useful - it could've just been adding entries to a tuple!

You could also use random values for the inputs, to verify (over subsequent runs) that it handles many different values properly. In this case you have to duplicate at least part of the production code to verify the result:

old_position = (random_number(), random_number(), random_number())
velocity = (random_number(), random_number(), random_number())
new_position = (
    old_position[0] + velocity[0],
    old_position[1] + velocity[1],
    old_position[2] + velocity[2],
)
boid = Boid(old_position, velocity)
boid.move()
assert_equal(new_position, boid.position)

In addition to these issues, there's the very real possibility that your position and/or velocity might be floating point numbers, in which case you probably want some assert_almost_equal variant for most of your assertions.

8
  • 1
    I kept it vague because the core logic should be loosely coupled to the coordinate system. You should be able to reuse it across 2D and 3D implementations. If new_pos = old_pos + vel is wrong, it's due to my poor grasp of physics. (Also, I imagined velocity to be a simple vector, like position.) Regarding new_velocity, this scenario tests that, every tick, a boid's position is updated according to its velocity. Ensuring its velocity is updated appropriately belongs elsewhere. Finally, regarding random numbers, I considered that, but I dislike the potential for flaky tests. Commented Dec 28, 2023 at 9:24
  • 2
    BDD doesn't hide details, OP their (example) usage of BDD hides details. There is no reason the scenario couldn't be Given a Boid at position 1,2,3, When it moves 0,1,2, Then its position should be 1,3,5. Commented Dec 28, 2023 at 10:07
  • 6
    @JoryGeerts BDD encourages hiding details, because it's meant to be written as human-readable English, which is then parsed very differently by brains and the underlying BDD statement parser. Of course you could write clear BDD statements, but at that point it would be at least as verbose as the underlying code. Commented Dec 28, 2023 at 10:25
  • 5
    @JoryGeerts, I have to agree with l0b0 here. BDD is not meant to be detailed technical specifications. I also think people are too focused on the particular scenario in the question. The idea is you express the behavior of the system as an end-user would understand it. I think this answer brings up a good point. There is a line where vital behavior is so finely detailed that it becomes a poor fit for a BDD scenario, because it is too low-level. Something this vital and low-level is a better fit for a unit test. Commented Dec 28, 2023 at 17:00
  • Or in other words: test at the appropriate level of abstraction, and given the example in the question, I do not believe a BDD scenario is the appropriate level of abstraction to verify this behavior. Commented Dec 28, 2023 at 17:00
4

You might have chosen a bad example.

  1. It should be new pos = old pos + (vel * delta time)
  2. You would expect this calc to apply to everything in the engine, not just the boids.
  3. "when it moves" should be "when time progresses" or something

So, No i don't think I would have the exact test you describe.

If I'm testing the flocking algorithm I probably want some tests on a group of boids. Do they stick together etc. That's the responsibility of that code.

I would expect the physics engine to abstract out the basic pos = velocity * time implementation, so that test would go on there.

An end to end test on the whole application might have that "the boids have to be moving!" test, but i doubt i would be looking at individuals or their exact speeds.

If you write your test as described it seems brittle. ie, you change the physics engine to add air resistance, or special relativity, or collisions with the ground. Now your flocking algorithm still behaves as desired, but the test fails because you are testing for this detail which is really a separate responsibility.

3

This looks like something which should be at least covered by unit tests. As for BDD, it depends.

As already pointed out, position and velocity are not directly compatible for addition; you've assumed a time quantum over which your velocity vector implies a delta position vector, which is compatible.

If position, velocity vector, and the time quantum of your physics model are all meaningful from an outside perspective, then BDD coverage would be appropriate. However, I suspect that time quantum is an internal detail, so this might not be appropriate for BDD coverage. However, as you say, this simple detail is critical to the overall functionality, so it should have unit test coverage.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.