Reading-notes - Test-Driven Development with Objects
21 Aug 2019
Reading notes based on Growing Object-Oriented Software, Guided by Tests by Steve Freeman and Nat Pryce
Most of the concepts I have written about are covered in the book, but they are worded and illustrated using my own examples. I capture the things that stand out as being most important (from my own perspective). If you read the book (it's great!) then you are likely to get insights that are unique to you.
A web of objects
The most important thing about Object-oriented programming according to Alan Kay, is not the internal properties and behaviour of objects, but how they communicate with each other.
Objects communicate by sending messages to each other. Alan Kay says he regrets calling Object-oriented programming a name that suggests the focus is on the objects, and not the messages themselves.
An object can only recieve messages that it can understand. Messages are essentially the name of methods, but the method themselves is how the object carries out the message that they have received. The method itself should be private to the object itself. Other objects can give it a message telling it what it wants the object to do, but they do not know how the object carries out the message.
An object-oriented system in a web of objects that work together by communicating with other objects in the system. The behaviour of the system is an end result of the communication between different objects, and this can look different depending on who the objects are collaborating with. Just like you might behave differently in a professional setting than you would with your close friends and family.
An important consequence here is that we can
change the behaviour of a system by changing the composition of its objects-adding and removing instances, plugging different combinations together - rather than writing procedural code (a recipe of step-by-step instructions).
This approach is known as a declarative approach, because we tell the objects in the system what we want them to do instead of how to do it (unlike step-by-step instructions. This makes it easier to change the systems behaviour in the future because we can focus on changing who the objects communicate with.
Values and objects
In object-oriented programming, there is a difference between value instances and objects that have their own identity. This can be a confusing difference unless the distinction has been learned, because in object-oriented programming, everything is an object, including numbers and strings.
Numbers and strings are examples of value instances. They do not have their own identity. To make this a little clearer, imagine you have a wallet of £5 notes and you are standing at a cash register. There is a long line of people behind you as you are trying to pay. You can use any of these £5 notes to pay for your shopping. It doesn't matter which one you use because while they are all different, they are all essentially the same thing and are interchangeable.
Whereas objects that have their own identity are not interchangeable. While you might have two people with the name Andrew, they are fundementally different.
Value objects are an important concept when it comes to testing object oriented programs. Most tests rely on comparing an expected result with an actual result. However, if you compare two instances of the same object with the same values, you will get a result saying that they are different, because all objects (even value objects) have their own id. Testing for object equality involves comparing their ids, so two £5 notes would be considered different. So it's important to put in place a mechanism that allows you to equate value objects as the same thing.
Follow the messages
In order for an object-oriented system to be easy to change, we must design our objects to be easily 'pluggable'. According to the authors,
this means that they follow common communication patterns and that the dependencies between them are made explicit.
A communication pattern is a set of rules that govern how a group of objects talk to each other: the roles they play, what messages they can send, and when, and so on.
OOOoo a really interesting thing mentioned in the book here is that the communication patterns include the domain model. My understanding is that the domain model is the language used by domain experts to talk about their domain. So the kinds of messages the objects send should reflect the same language that would be used by domain experts. This would make the program itself read a lot more like english. The book didn't outline this exact impression but triggered this click moment.
Thinking about a system of objects in terms of the messages they use to communicate with each other is a significant shift from how most of us are taught when we are introduced to objects.
In relation to this, the authors say that
tests and mock objects help us see the communication between our objects more clearly. I'm looking forward to learning more about this later on in the book, yayyy.
A useful way to think about objects is in terms of roles, responsibilities and collaborators (Wirfs-Brock and McKean, Kent Beck and Ward Cunningham).
- Roles: A set of related responsibilities.
- Responsibilities:An obligation to perform a task or know information.
- Collaboration:An interaction of objects or roles (or both).
It can be useful to use index cards to draw out your object's role, responsibilities and collaborators using a design tool called CRC cards. This allows you to explore the potential object structure of your system without getting stuck in detail or building yourself into a corner.
Tell, don't ask
When it comes to figuring out what the messages that objects send to other objects should say, we should let the
calling object define what it wants in terms of the role that it's neighbour plays, and let the called object decide how to make this happen.
One benifit of this way of writing messages is that it is easy to swap out objects that play the same role. You're just telling them what you want to do but not how to do it. So if the object isn't there the calling object doesn't break, the message just does not get received.
Another benefit of this approach is that it forces us to be very clear on exactly how we want the objects in our system to collaborate with each other. The behavior of each object is self-contained, they are in charge of the things they do. They can send messages to tell other objects that they want them to do something, but they don't actually do it for them. This makes a lot of sense, but is very different to the procedural way that most programmers are taught.
But sometimes ask
Sometimes, we do ask objects for information for access to their internal state (instead of telling them to do something and return the result). Some examples of this include creating new objects and getting information from values and collections (an array is one example of a collection).
Asking objects for information can be called 'querying', and is something that we aim to do sparingly, with a clear name that makes it obvious what we are asking for. We do this sparingly because accessing an object's internal state makes the system a little more rigid. If you remove that object, then the query request becomes broken. Whereas a tell only message does not break if the receiver is removed, it just doesn't do anything.
Unit testing the collaborating objects
An object-oriented program consists of objects that can only interact with each other by sending messages to each other.
When we have objects that only communicate with each other by sending messages to each other, the focus of unit testing changes to reflect that.
The main focus of our test becomes making sure that objects communicate with each other as we expect them too. We can test this by making sure that it's collaborators have received the messages they are expecting to receive.
As the focus is on testing how our target object communicates with each other, we don't actually need it's collaborators to exist inside of the unit tests themselves. Instead, we replace the collaborators with mock objects that implement only the behaviour that is needed to make the test pass.
Support for TDD with Mock Objects
The steps that are involved in writing a test with mock objects are:
- Create any required mock objects
- Create any real objects, including the target object
- Specify how you expect the mock objects to be called by the target object.
- Call the triggering method(s) on the target object.
- Asserct that the resulting values are valid and that all the expected calls have been made.
The authors say that the most important thing is making
clear the intention of every test, distinguishing between the tested functionality, the supporting infrastructure, and the object structure.