Unit testing at the speed of light with Unity Test Tools
It’s time to tell a little bit more about NSubstitute library that ships with our Unity Test Tools and patterns of it’s effective usage.
Each software system consists of units of functionality. In object oriented languages, the minimal unit of functionality is a method. These methods usually depend on other methods and classes. If you have to test a method, some challenges will arise.
- First challenge is that external dependencies are not easy to set up, e.g. some objects with complex initialization might be required.
- Second challenge is that the test verifies specific execution path that requires certain behavior from other classes that are used.
- Finally, calling methods of external classes might lead to some changes in an environment that could not be rolled back, e.g. deleting a real record from a database.
Unit test is about testing unit of functionality in an isolated environment. Isolated means with all dependencies mocked up. Which means that test works in a test specific environment where only one execution path is possible.
Test doubles substitute real dependencies of unit under test, forming test specific environment and making unit tests fast and robust.
There are five test double patterns: Dummy object, Test stub, Test spy, Mock and Fake.
This article uses a simple Space Trader game clone to demonstrate the usage of test doubles and the NSubstitute library. This game is centred around the spaceship controlled by the player.
The player can equip weapons to fight pirates. The spaceship has weapon slots and a weapon can be equipped if an empty weapon slot is available.
Test scenario: make sure that weapon slot is occupied when weapon is equipped.
- Take a spaceship with one free weapon slot
- Take any weapon
- Equip the weapon
- Check that no weapon slots are available
Test with dummy object created manually.
Test with NSubstitute based dummy object
Weapon object is nominal in this scenario which means that its methods and properties are not intended to be used in this execution path. It is just a required parameter and anything that implements IWeapon interface can be used, including null when method has no null parameter check. This object usage is called dummy object pattern.
There are two approaches to create dummy objects.
The first approach is to create dummy object manually. It makes sense to have only one dummy object per real object/interface and IDE functions will help you to generate classes that implement interface. Creating dummy objects this way and storing them with your tests is not a big deal.
The second approach is to use NSubstitute that is shipped with Unity Test Tools:
Remember that passing null as a parameter when method has no null parameter check is also considered to be dummy object pattern. Null is not descriptive though.
All described methods are valid and very easy to use.
But what if spaceship’s method should return some value and this value is taken from weapon object? In this case, weapon is not a Dummy object anymore. It’s time for a test stub.
Test stub returns values for testing one specific execution path. E.g. BrokenWeaponStub, IncompatibleWeaponStub and others that let you test specific scenarios.
Test scenario: make sure that ship shoots at least one shot if functional weapon is equipped
- Take a spaceship with a single weapon slot;
- Equip weapon;
- Check that spaceship’s round contains at least one shot;
Test with test stub created manually.
FunctionalWeaponStub implements IWeapon interface but return hardcoded value.
Unlike Dummy object that does nothing, Stub contains hardcoded values and is intended to return them as this values result in specific execution path.
It is possible to create the same stub using NSubstitute:
The object created by
IWeapon interface and is returned as
IWeapon type. But it is actually a proxy object whose behaviour could be changed.
Returns is an extension method of NSubstitute library that changes the behavior of this proxy. When Shoot method is called, the value specified in
Returns() method is returned. It is also possible to provide a sequence of values or a delegate.
A sequence of values would be returned In the example below.
First call to
randomNumberService.Range() with any parameters will result in 0. Next call will return 2 and so on.
Other useful function of NSubstitute is the ability to match arguments by template.
In the example below the default behavior would be overridden making any call to Range(10,100) return 80. For more details read the NSubstitute manual.
The approach with random number generator stub is highly effective in testing random events, because it is possible to emulate required sequence of random numbers.
Making stubs manually is easy and naming them correctly leads to creation of clean and readable tests. NSubstitute can decrease the amount of code and provide additional flexibility.
But what if it is needed to log the behavior of an object? How many hits has an opponent received? If the test double has logging functionality, then it is called a Test Spy.
Encounter means meeting someone in space en route to a star system. The player can choose an action to take in an encounter. For example, the police could be ignored and a pirate attacked.
Test scenario: make sure that opponent gets hit in an encounter
- Take an opponent;
- Take a spaceship with two weapons slots;
- Equip two weapons;
- Create an encounter;
- Choose attack action in the encounter;
- Check that opponent got hits;
Encounter requires two spaceships and a random number generator to calculate probable outcomes.
AlwaysMaxRandomNumber is passed which will always return maximum number and this will leave no chances for the opponent to evade the hit. The test is following a very specific execution path.
The player spaceship is real in this encounter, but the opponent is a spy. The spy combines stub and logging functionality. It logs the hits that would be checked later in the test.
NSubstitute uses special argument matcher Arg.Do to execute a delegate when argument to it passes to the substitute. Which means that
hitCount+=x.Count() would be executed on each method call.
The spy only logs the data, leaving verification to the programmer. What will happen if a spy is given the right to verify the data?
Test scenario: make sure that each weapon shoots when spaceship shoot method is called
- Take a spaceship with two weapon slots;
- Equip two weapons;
- Check that each weapon got call to Shoot method.
This test case is special, not just because it uses Mock object pattern, but because it verifies the behaviour of the spaceship instead of the state. (Learn more about London and Chicago schools of TDD)
The test does not care about returning values of test doubles, it makes sure that certain methods with certain parameters were called. We use an assumption that if a spaceship has called correct methods with correct parameters on external systems, then it works correctly.
Making mocks manually might be rather time consuming. Using NSubstitute framework to get mock objects for cheap is a good choice.
NSubstitute can also verify if methods were called in a specific sequence.
A mock object not only logs the calls but also verifies them. It is a test spy with built-in assertions.
But what if a test double needs some logic?
A test double that contains logic is called a Fake object and it is a dangerous beast. It is the only test double that contains logic and emulates a real system component. As the Fake itself is complex, it is used to substitute real system components which could not be substituted by stubs. The most common example is the substitution of a real database with an in-memory database or using fake web service instead of a real one.
The best solution is to avoid fake objects when possible, as they know too much about the behaviour of the component they substitute and contain logic that should be tested and is a subject to change as well.
As a brief conclusion:
- Making stubs, dummies and spies manually is not complex and giving them proper names will make code readable and clean.
- NSubstitutes is designed for mocking and using it is preferable to designing mock objects by hand.
- Checking the behaviour and state based testing are both powerful. Use the one that makes the test simpler.
- Avoid fakes (logic) in test doubles when possible.
Happy unit testing !
Examples are on the GitHub.
Different books use different terminology see terminology in books
NSubstitute is very powerful and taking a look at the documentation is a good idea.
Good principles to follow.
If you still don’t feel the difference between mocks and stubs read “Mocks aren’t Stubs”.
Arrange Act Assert (AAA) pattern was used to structure unit tests in the article.
There is an open source implementation of a Space Trader game worth playing with.
21 ComentáriosInscrever-se nos comentários