The course went so comprehensively into the subject in just a few hours, and I could make only the briefest of notes between the problem-solving tasks (unit testing a Fibonaci sequence function, testing for ordered arrays, etc.). There’s a lot of material on the Codemanship site, though.
There are two reasons given for why we might want to unit test. Firstly we want to start the coding stage of development by creating unit tests that define our software’s behaviour – the tests should be prescriptive and not descriptive of the behaviour of the software we develop. This is the basic principle of Test-Driven Development (TDD). Secondly, the theory goes that the amount of change required to resolve defects increases sharply the longer they’re undetected. If a defect is found during a module’s creation, one only needs to rectify that module instead of multiple software components.
What wasn’t mentioned was the duplication of effort that comes with TDD, at least in the short term. You’re adding roughly double the amount of code (if not more) for each module. What I also learned from test automation is that test scripts require their own management, maintenance, version control and documentation. They become their own projects.
Anyway, the unit testing we’re familiar with here follows the xUnit class of framework. Apparently it is possible for anyone to develop their own framework with some programming skill. There are three common testing frameworks for Visual Studio:
With a little further research, I found that ‘xUnit’ is a general term for unit testing frameworks that follow a general model: there is C-Unit, CPP-Unit, J-Unit, etc. Unit testing with an xUnit framework involves creating methods that follow the ‘Arrange-Act-Assert’ pattern. This means the initial lines determine the test parameters, the lines following that execute the unit being tested, and the final lines contain the assertions for comparing the expected results against the actual results. It should look something like this:
There is a collection of assertions that could be used, such as:
Another characteristic is a unit test should have no external dependencies, as the modules are being tested in isolation. We can use other test-related services to mimic the input.
Test methods have the
[TestMethod] decorator. These are within a class of test methods, the class having the
[TestFixture] decorator. A ‘test suite’ consists of all the classes that form the test run.
It should follow this structure:
To get started with unit testing, add another project called ‘[ProjectName].Test‘ to the solution, and install the NUnit Framework package to it using NuGet. Then create a new class, and the import statement ‘
using NUnit.Framework‘. Without ReSharper installed, you’ll need to install NUnit Test Adaptor for the solution.
Parameterised and Data-Driven Tests
This is a way of running the same methods multiple times with different values. It removes the need to duplicate the test methods. Simply change the values within the [TestCase] declaration.
public void SquareRootOfPositiveNumber(int input, int expectedRoot)
Data-driven testing is based on the parameterisation concept. It is also referred to as ‘property-based testing’.
([Range(1, 100, 0.1)] double input)
([Random(1, 1000, 100)] double input)
NUnit includes the Fluent API that enables the use of more verbiose unit test scripts. Whether Fluent scripts are easier to understand is a matter of debate – they could be more readable to the layman, but less so for the developer.
As Fluent assertions are more expressive and verbiose, they should be easier to debug.
Unit Test Patterns
Several design patterns for unit tests were covered briefly, but they follow SOLID principles in some places. This part of the session covered inversion of control, interfaces and dependency injection. With this the following could be achieved:
- Setup code re-use
- Setup code encapsulation and availability through simple API
- Test code extension and re-use
- xUnit patterns
- Contract tests
- Test data builders
A double provides an interface that, for the purpose of testing, replaces something the software would normally use. These include:
- Stub: Provides data to the unit under test.
- Mock: An object that causes a test to fail if a method isn’t invoked on the expected interface.
- Dummy: An object required for the test to run, but is not important.
- Fake: Emulates the full behaviour of the expected object. For example, an in-memory database.
- Spy: A stub that records which methods were called.
Since interfaces can allow for different implementations of a single action, they are ideal for impersonating compnents and data providers the unit under test might interact with.
A stub should be very simple, and include little or no logic. Stubs should not be tested.