Unit test tutorial (1/3) Fundamentals

Unit test is basically protecting code from yourself (from people in general). Let me show some example in this language independent unit test tutorial. We all do mistakes, we all thinking focused around specific bulk of things at a time (even in this Unit test tutorial as you will see). We cannot keep eyes on every aspect of a system over and over again, but unit tests do.

Unit test tutorial

Typically it comes to unit testing, when a system you develop becomes more and more complicated, so you need a solid understanding of how things work. Also when you need extreme stability, an unquestionable code trust.

In a recent case of mine, I used unit testing as a debug method, as I factored out pieces of working code to distinct units, and have them proven one by one. So this method left me with the code pieces those carried the error I could not find otherwise.

After that, I actually added new feature even more gently, so first I created tests I wanted to pass, then implemented the feature after that to see the test passing (also known as :TDD:). It gave me great inshights on how the class / method interface should look, what potential error-prone situations may arise.

Unit test example – Hello World!

To begin with unit testing, you don’t need specific unit testing frameworks. Just pick a unit, and test it. Having this simple unit test hello world below (written in C# / Unity), you can see the fundamentals.

int Add(int a, int b)
{ return a + b; }

void TestAdd()
{ Debug.Log((Add(1, 1) == 2) ? "Pass" : "Failed"); }

You may add some more tests, like add negative numbers, but as long as you run TestAdd() you can be sure that Add() just really did not broken during the course of development. If you experiencing errors throughout your code, it would have nothing to do with Add().

Unit test example – Making units

In the unit test exaxmple below, testing makes a geometric library much more robust from the really beginning. Lets assume you’ll need a method to normalize a 2D vector (written in C# / Unity).

Vector2 Normalize(Vector2 vector)
{
	float length = Mathf.Sqrt(Mathf.Pow2(vector.x) + Mathf.Pow2(vector.x));
	return new Vector2(vector.x / distance, vector.y / distance);
}

Using this code you may experience that the calculation is broken, even in very simple cases, like (4, 0). You would expect a nicely normalized (1, 0), but you get something like (0.707, 0) here (the result really seems like sine of 45°, so it will be probably repeating coordinates anyway).

So lets make units first. It seems vector normalization built from two distinct calculation, so do some factoring. Really just copy paste around parts of code.

float Length(Vector2 vector)
{ return Mathf.Sqrt(Mathf.Pow2(vector.x) + Mathf.Pow2(vector.x)); }

Vector2 Normalize(Vector2 vector)
{ return new Vector2(vector.x / Length(vector), vector.y / Length(vector)); }

Actually you can do more, something like below.

float Length(Vector2 vector)
{ return Mathf.Sqrt(Mathf.Pow2(vector.x) + Mathf.Pow2(vector.x)); }

Vector2 Divide(Vector2 vector, float divisor)
{ return new Vector2(vector.x / divisor, vector.y / divisor); }

Vector2 Normalize(Vector2 vector)
{ return Divide(vector, Length(vector)); }

The unit part of unit tests is done. Look how factoring (teamed with proper naming) make Normalize() method much more readable. Now do the tests part.

Unit test example – Making tests

Sticking with the example above, lets make test the first unit Length() with the vector (4, 0). To do this, first you have to mock a context for the test. For this really small piece of example, the mocked context will be a simple vector variable. The length should be 4, so make an assumption for that.

void TestLength()
{
	Vector2 mockVector = new Vector2(4, 0);
	bool assumption = Length(mockVector) == 4;
	Debug.Log("Testing Length() with (4, 0) "+((assumption) ? "succeeded" : "failed")+".");
}

This three line carries everything a test case should have. A mock context (a single vector variable), an assumption (an assumed length value of the vector), and some UI to communicate the results (the nicely designed log message).

Testing Length() with (4, 0) failed.

Being lucky, this test immediately fails. When using unit test for debugging, broken tests are gold. That means you nailed down the unit carrying the erroneous part.

Typically you make the system stronger by altering the corresponding implementation to pass the broken test. In addition, that test will stand there for the end of the times ensuring that implementation won’t break again that way. If you cannot see the where the implementaion fails, you can still make smaller units. Here we could factor out Mathf.Sqrt() and Mathf.Pow2() methods. Being framwork methods, those tests would pass of course, so we would left with the broken Length() implementation again.

Keep in mind that to make all this work, you have to assume the right things. If you assume that the length of (4, 0) is 2, then you’ll never get a working codepiece. This is the main reason behind the practice that suggest to make really small / encapsulated piece of units when making unit tests. If you put a whole lot of logic into a unit test, or into a mocked context, it can be broken by itself even before it does something useful.

Now taking a closer look to Length() you immediately spot the redundant use of x coordinate, so as soon as you fix it, the test will succeed.

float Length(Vector2 vector)
{ return Mathf.Sqrt(Mathf.Pow2(vector.x) + Mathf.Pow2(vector.y)); }
Testing Length() with (4, 0) succeeded.

Wow. Having all this you probably already got the basic idea. Now as you need to test more and more implementation, and add up more and more test cases, you’ll need helper methods, and some output formatting to easily proceed with testing.

Also as your tests grow, you’ll want to group test together into separate files, also want to run them on occasions only. So you may add some extensions to the code, like pimp UI output, add different assumption types beside testing equality, helpers for mocking contexts, further improving code testability in general on implementation side. In the next part of the series I’m gonna cover more details on those aspects.

As an alternative, instead of creating something that fulfills your specific needs, you can always simply google for a suitable unit test framework of your choice. In general they will all do something really similar to we have done all above.

DISCLAIMER. THE INFORMATION ON THIS BLOG (INCLUDING BUT NOT LIMITED TO ARTICLES, IMAGES, CODE SNIPPETS, SOURCE CODES, EXAMPLE PROJECTS, ETC.) IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE INFORMATION ON THIS BLOG (INCLUDING BUT NOT LIMITED TO ARTICLES, IMAGES, CODE SNIPPETS, SOURCE CODES, EXAMPLE PROJECTS, ETC.).