We left our project with an integration test that doesn’t pass (yet), and a unit test for the first piece of business
logic revealed by this integration test: the character races as “types” for our Character
objects. It is now time to
make the unit test pass, which should drive us to an implementation of the Race
class, and then move on to the next
encounter in our integration test.
Ancestry shenanigans
Our unit test, at the moment, looks like this:
And, as expected, it fails. More precisely, once we add a placeholder Race
class to satisfy the require
instruction,
it fails with this interesting error: class or module required
.
Back in the previous chapter, I said that a true mockist would probably not use the Race
class in the example, but
instead something simpler, like Object
. Well, this wasn’t completely accurate. Because we want whatever object
is passed to #choose_race
to define what a Character
object “is”, this object has to be either a class or a module,
as the error message tells us.
Let’s pause for a second. Our test is driving our design. Writing it, we discovered that we want characters races
to be instances of some kind of Race
class (race=Race.new
). But, at the same time, our test lead us to a design
where these instances must be classes or modules themselves (assert_kind_of race
, with assert_kind_of
expecting
a class or a module). Can an instance also be a class (or a module)?
Of course it can! This is one of the great (and elegant, and almost magical) things about Ruby: classes are instances,
too – namely, instances of the Class
class. By the same principle, modules are instances of the Module
class, or a
subclass of it. Therefore, we can move on to the next failure in our test by ensuring that Race.new
returns either a
class or a module. The simplest way to do that would to make Race
inherit from either one – except that Ruby doesn’t
allow subclassing Class
, so the only option left is to have Race
inherit from Module
, so that Race.new
returns
a (new) module.
We’ve successfully failed – meaning that we’ve successfully moved on to a different failure. But this one is a bit cryptic.
And how could our object be both “a kind of” Character
and “a kind of” Race
? Object in Ruby can only be of a single
class, right?
Without diving too deep in the (marvellous) object model of Ruby, let’s make a slight detour. The assert_kind_of
matcher relies on Object#kind_of?
, which is defined
like so:
kind_of?(class) → true or false
Returns
true
if class is the class of obj, or if class is one of the superclasses of obj or modules included in obj.
This is very accurate but maybe a bit obscure, if you’re not familiar with the way classes, modules and instances
work in Ruby. Another way to define kind_of?
could be:
Returns
true
is class is among the ancestors of obj’s [singleton] class.
Let’s ignore the word in brackets for now. In Ruby, we know that each object has a class; this class, like all
classes, inherits from another class, which itself inherits from another class, and so on until this chain of
ancestors reaches BasicObject
, “the parent class of all classes in Ruby”.
We can check this out by looking at the ancestors of the class of the character
object in our test. We know
that this object’s class is SteelVellum::Character
, so we can do it like this:
Ignoring the first item in this array (which is the interrogated class itself), we see the list of classes1 from
which Character
inherits: Object
, Kernel
and, eventually, BasicObject
. For our test to pass, and
our assertion assert_kind_of race, character.character
to be true, we need to somehow add race
to this list of
ancestors.
We cannot do that by making our race
object a parent of the Character
class – first because it would make no sense
from a business logic perspective (characters are not character races), but more importantly because we’ve already
established that race
is a Module
, and modules cannot be inherited from.
However, like classes, Ruby modules can be part of the ancestors chain of a class – in fact, in the ancestors list
above, Kernel
is actually a module, not a class. As explained in
the definition of Object#kind_of?
, included modules also count as ancestors. But how could we include this race
module in the class of our character
object?
A single-use class
Once again, the character
object is an instance of Character
. So, a naive way to have it also be “a kind of” race
would be to call Character.include race
. But then, all instances of Character
would also have race
in their ancestors.
All characters would be of the same race, which is not what we want.2
What we want is for this race
module to be included in the class of our character
instance, but only for this
instance. And we can do that thanks to Module#extend
and the elegant magic of the singleton class.
By extending the instance with the module instead of including the module in the class, we’ll have what we want. To see this in action, let’s temporarily hack our test:
Or test passes! But how come?
When we called character.extend race
, Ruby did something clever. It created a new class, anonymous, and had it
inherit from Character
. It also included race
into this new class, and then had character
inherit from it. Because
it inherits from Character
, this anonymous class behaves exactly as Character
, but it is specific to the character
object. (And it includes race
, which is the whole point.)
There is no formal name for this kind of object-specific, anonymous class. Some used to call it “eigenclass”, others
“ghost class”, but nowadays, it is most often named singleton class3. In fact, this class can be reached (and
created on-the-fly, if necessary) by calling Object#singleton_class
.
Let’s launch an IRB console and compare the ancestors of this singleton class, for a given Character
instance, before
and after extending a Race
module:
So, there you have it. Even though Ruby objects can only be instances of a single class, they can inherit traits from any number of modules, and don’t have to share these inheritances with any other object, thanks to the existence of a singleton class.
Now that we know how to have characters be of a given race, and why this is even possible in the first place, let’s remove the hack from our test and implement things properly:
Back to the outer loop…
Our unit test now passes – we’ve closed the small loop. Let’s go back to the big loop (the integration test) and see where the next failure leads us.
This was to be expected – CharacterCreation#choose_race
must be passed a module now, but MountainDwarf
is still a
slimed class. Let’s change that.
Moving on, we can rerun the integration test and figure out what other missing piece of our library we should build now.
This is a more interesting failure! According to the test, making Bruenor a Mountain Dwarf should automatically give him
a :medium
size, but at the moment, Character#size
always returns nil
(since we didn’t bother actually implementing
the method’s body). Let’s remedy that.
Because this failure reveals a missing piece of business logic, we must start a new small loop, and design the implementation of this unitary feature through one or more unit tests.
Hooks in you
For a start, let’s simply isolate the failing assertion from the integration test into an unit test:
Covered by our unit test, let’s think about a way to make it pass. The test tells us that, once a Character
instance
is extended by the MountainDwarf
module, its #size
method should return :medium
instead of nil
. When a module
extends an object, the methods defined inside this module are added to the instance methods of the object’s singleton
class, so one way to make our test pass would be to redefine #size
in the MountainDwarf
module:
However, while perfectly fine in general, I’m not too fond of this approach in this specific situation. That is because a character’s size is more data than behavior. I’d rather store this information in an instance variable than have it being returned by a method4.
Thankfully, Ruby gives us another trick to reach our goals: hook methods. These are methods that, if defined,
get called we certain events happen in an object’s lifetime. For example, #method_missing
is a well-known hook
method that is called when an object (or rather: a module or a class) receives a call to a method that neither
it not any of its ancestors define. In our case, we’ll make use of the #extended
hook method.
This method is called whenever a module extends an object. We can use it to change the value of the character’s @size
instance variable – in practice, giving it a default value, which the Character
instance will then be free to change,
if need be. (After all, our Dwarf could one day drink a magical potion and grow a size or two.) This is what using
the #extended
hook looks like:
Of course, for the character.size = :medium
instruction to work, we need to give accessors to the @size
instance
variable of Character
:
Now our test passes. We can close this small loop and go back, once again, to the big one by running (yet again) the integration test. It now fails because of the next character trait that a race is supposed to give a default value to:
This time, it is Character#speed
that doesn’t return the expected value. We’ll proceed as exactly like we have with
#size
– adding a unit test, watching it fail, making it pass, and then moving back to the integration test. And after
that, we’ll have Character.darkvision
to fix. In the end, this is what our MountainDwarf
class and its tests will be:
Stepping away from BDD
Normally, keeping with our back-and-forths between the integration tests and the unit tests, our next step should probably be
have to do with ability_score_increases
. However, once again, I’d like to take a step back and consider
our recent work.
We’ve implemented the behavior of the Races::MountainDwarf
instanciated modules, because this is what our tests have
covered. But we know that other races will eventually be covered by the library, and we know that they, too, will
assign a size, a speed and a darkvision range to the characters. So, even though we don’t have any test to lead us
there yet, we can safely assume that making this piece of business logic a bit more generic is valuable.
In practice, this means that any subclass of Race
should be able to assign values to a Character
’s @size
, @speed
and @darkvision
instance variables, and the assigned values would depend on the subclass itself. This is rather easy
to write tests for.
First, we need to be able to define the values that a race will assign:
Then, we need to ensure that using the race to extend a Character
assigns these values. We can simply cannibalize
the tests for MountainDwarf
; but for the sake of conciseness, we’ll squash the 3 tests into a single one with
multiple assertions:
The implementation is pretty straightforward too, except for one subtlety:
The #extended
hook method must be defined in the class (singleton or not) of the object on which it will be called.
This is why, when its definition was in the MountainDwarf
class, it was sent to self
. (In other words: .extended
was defined as a class method of MountainDwarf
). However, since we’re moving this definition up to the
class of all races modules, the #extended
must now be defined as an instance method5 of Race
.
(Note also that we’ve also added default values in the initializer, even though we didn’t write tests for that, and therefore have no idea if this is legitimate design or not – we’re freewheeling! 🤘)
Hidden edge cases
Here is a secret about BDD: since it’s about letting the expected behavior drive the design, edge cases – in other words: unexpected behavior – can slip through. Which is why it is important to consider these edge cases when working at the unit test level, where they are easier to think about.
In our case, even though we’ve kept saying that a character’s race gives it default values for some traits, we haven’t
tested for the (unlikely) situation where some would have already been defined before the race was assigned. So let’s
add that. And while we’re at it, let’s cover another edge case: using a race module to extend an object which is not
an instance of the Character
class.
The final implementation is quite easy:
Final cleanup
Now that the logic for assigning default values to a character’s racial traits is moved up to the Race
class from
which MountainDwarf
inherits, we can clean up our previous work, by deleting the now redundant unit tests in
MountainDwarfTest
, and the logic from MountainDwarf
:
And this is it (for now)! We’ve successfully implemented the first actual piece of logic in our library, which is actually quite a lot:
- We can define a character race, or at least 3 of its traits for now.
- These traits are automatically assigned to a character when their race is chosen during character creation.
- For developers who’ll eventually use our library, assigning a
Race
to aCharacter
object gives it some kind of “type”, which is probably a false good idea, but fun nonetheless.
We can now return to our big loop, once again, and see what the DM of BDDing has for us in the next installment of this series!
-
Roughly speaking. ↩
-
Feel free to try this out in an IRB console: create 2 instances of
Character
, create a newRace
, include it inCharacter
withCharacter.include the_new_race
and see that both the instances now “are” also of this race. ↩ -
Don’t be mistaken, this class has nothing to do with the singleton design pattern, or the Singleton module! ↩
-
Technically, even if store in an instance variable, the value will be returned by a method (namely, a reader accessor), but hopefully you see what I mean. ↩
-
As an exercice, can you guess what would happen, and why, if within the
Race
class we’d writedef self.extended
? ↩