Unit Testing - Dawn-of-Light/DOLSharp GitHub Wiki
- replace "Edit and Pray" with "Cover and Modify"
- more reliable than manual testing
- errors often are discovered immediately e.g. off-by-one or wrong sign
- easier to reason about errors when a test fails
- documenting intent
- run quick (preferably less than 100ms)
- do not depend on external sources (e.g. file system, data base, external code that is not under our control)
- deterministic (no random)
- do not depend on each other (meaning no other test must run in order for the test to succeed)
- narrow scope (often only a single line of a method) which ideally tests one thing only
- it should be easy to tell what the test is doing
- unit tests should contain no logic (!) meaning no
if
,switch
,for
etc
Note: In Legacy code most unit tests are characterization tests. We bring the existing behavior in a test vice, so we can verify that our changes didn't break existing behavior instead of editing and praying that it works. Unit testing is also a good way to understand existing code. It is like an evidence-based repeatable interrogation. Of course it is important that you ask the right questions.
RED-GREEN-REFACTOR-(repeat)
RED: create a failing test (make the test at least compile and fail with a sure wrong value)
GREEN: make the test pass
REFACTOR: make the code pretty (do not change behavior)
In an ideal world you write the test first and implement the feature/behavior change afterwards. This has a lot of advantages, since you can reason about the design of your method/class before you actually implement anything.
Tests are supposed to be executed after every change disregard if you change behavior or not, that is also the reason why they have to be quickly executed.
- unit tests are in a separate namespace in the "Tests" project
- test class names and test file names are prefixed by
UT_
so you can easily spot them in the IDE and distinguish them from the production class (file) - fakes that are used by more than one TestFixture are in a separate file; special fakes are in the same file
Unit tests names are not bound to the same character length limitations that your production code is. This is largely due to the fact that unit tests are only called by the test runner and therefore their names are never repeated anywhere in your code. It is still a good idea to be concise, but for readability you can and should be more verbose than in your production code.
Convention: <UnitOfWork>_<StateUnderTest>_<ExpectedBehavior>
(Credit: Roy Osherove)
UnitOfWork: What are you exercising? This is in most cases the method name.
StateUnderTest: Current (relevant) state of the object and/or parameters supplied to the method
ExpectedBehavior: Return value, exception, state after test
I have a slight variation of this sometimes: <UnitOfWork>_<Parameter>_<RelevantObjectState>_<ExpectedBehavior>
Which can make the text a bit easier to digest. It is basically just another underscore which is optional.
Unit tests should adhere to a certain order of execution so an inclined reader finds everything quickly.
Arrange: Prepare everything in order to be able to "Act"
Act: The actual action that are performed to get a/the result
Assert: Challenge the result with what you expect.
For better readability make them a paragraph each.
You want to test only one thing, so you should usually have only one assert. There are exceptions to this of course, because you can divide a long output (e.g. a full post address comparison or list entries) into multiple asserts or if multiple states are somehow related. This is a rare exception and you should aim for one assert.
Unit tests do not adhere to the same standards as production code. So you see broken encapsulation, empty methods, repetition, absurdly long method names.
Most OOP guide lines still apply, however to a lesser degree, since the test should be mostly self-explanatory. A lot of this is done through naming, so you can and should extract methods to avoid repetition (and especially to make your tests maintainable), however they need to have a good name. If you can't come up with a good name, don't do it and just make it verbose.
Legacy code is defined as code that is not covered by unit tests. This means that we need to create tests for existing code, instead of before we implement anything. The RED-GREEN-REFACTOR mantra still applies.
The difference is that the hard part is often to get to RED:
- Make the class under test initializable without calls to external dependencies
- Make method under test executable
- Use a bogus value for the expected value (e.g.
-2
) - Check that the actual return value or state value actually makes sense
GREEN: If the value makes sense, just change the expected value to the actual.
REFACTOR: You might now change the code under test, however it is likely that you need more tests in order to be relatively safe to refactor.
See: "Working Effectively with Legacy Code" from Michael C. Feathers for safe refactoring techniques, especially the ones that can be safely done without testing.
The definitions for these vary a lot and I recommend only two of them - fake and spy - for clarity.
Stub (not used): is a static implementation. So either a fixed value return or empty method body for void
Mock (not used): is often meant to be a catch-all for Stub, Spy, Fake, however it is semantically indistinguishable from "fake", so "fake" is used instead
Fake: is a configurable stub basically, but it can be used for stub/mock/spy as well
Spy: is a fake that can record interactions, which includes also invisible state changes
These have the same requirements as unit tests in that they have to be instantiated quickly, without access to external sources and should be named in a way that makes them easily understandable. Try to avoid logic in fakes, as long as it is possible and still clear what it does.
[TestFixture] //NUnit attribute for test class
class UT_StatCalculator
{
[Test] //NUnit attribute to mark a Test
public void CalcValue_L50Player_With100ItemBonus_Return75()
{
//Arrange
var player = NewFakePlayer();
var someBonusID = eProperty.Constitution; //StatCalculator doesn't seem to care for ID
player.Level = 50; //set Level explicitly
player.ItemBonus[someBonusID] = 100;
var statCalc = NewStatCalculator();
//Act
int actual = statCalc.CalcValue(player, someBonusID);
//Assert
int expected = 75;
Assert.AreEqual(expected, actual);
}
}
(In a normal test avoid comments, since your tests should be self-explanatory.)