Lesson 2 - Testing in C# .NET - Introduction to unit tests
In the previous lesson, Introduction to software testing in C# .NET, we made a relatively solid introduction to the issue. We also introduced the v-model that illustrates the relationship between the individual design phase outputs and the relevant tests.
We always write tests based on design, not the implementation. In other words, we create them based on the expected functionality. It can be obtained either directly from the customer (and that's in the case of acceptance tests) or from the programmer (architect), where he specifies how different methods should behave. Today, we're going to focus on these tests, that are called unit tests, and which test the detailed specification of the application, thus its classes.
Remember that we never write tests according to how something is programmed inside! It could very easily seduce our thinking in just one way, and we could forget that the method might get some other inputs which it isn't expecting. In fact, testing isn't related with implementation at all, we always test whether the requirements are met.
Which classes to test
Unit tests test individual methods in classes. For sure, I repeat that it doesn't make much sense to test single-purpose methods, for example, in models that just pick something from the database. To be more specific, there is no point in testing a method like this:
public void InsertItem(string name, double price) { using (var db = new DatabaseEntities()) { db.items.Add(new Item(name, price)); db.SaveChanges(); } }
The method inserts an item to the database. Typically, it is used only in a form, and if it does not work, acceptance tests will find it out as the new item would not appear on the list. There are lots of similar methods in the application and we would unnecessarily waste time by covering something that we can easily cover in other tests.
Unit tests are most often found with libraries, that is, the tools that the programmer uses in more places or even in multiple projects and should be 100% functional. You may remember when you used a library downloaded, for example, from GitHub. Most likely, there were also tests included. For example, if we write an application in which we often need some mathematical calculations, such as factorials and other probability functions, we create a library, for sure, for these calculations, and it's a good idea to cover such library with tests.
Example
As you probably expect, we'll create such a class and test it. In order to save time, let's just create a simple calculator which will be able to:
- add
- subtract
- multiply
- divide
Creating a project
In practice, there would be more complicated calculations in the class, but
we won't deal with it here. Create a new project, a console application named
CalculatorApp
. Add a public
class
called Calculator
with the following implementation:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace CalculatorApp { /// <summary> /// Represents a simple calculator /// </summary> public class Calculator { /// <summary> /// Adds 2 numbers /// </summary> /// <param name="a">First number</param> /// <param name="b">Second number</param> /// <returns>Sum of 2 numbers</returns> public double Add(double a, double b) { return a + b; } /// <summary> /// Subtracts 2 numbers /// </summary> /// <param name="a">First number</param> /// <param name="b">Second number</param> /// <returns>Difference of 2 numbers</returns> public double Subtract(double a, double b) { return a - b; } /// <summary> /// Multiplies 2 numbers /// </summary> /// <param name="a">First number</param> /// <param name="b">Second number</param> /// <returns>Product of 2 numbers</returns> public double Multiply(double a, double b) { return a * b; } /// <summary> /// Divides 2 numbers /// </summary> /// <param name="a">First number</param> /// <param name="b">Second number</param> /// <returns>Quotient of 2 numbers</returns> public double Divide(double a, double b) { if (b == 0) throw new ArgumentException("Cannot divide by zero!"); return a / b; } } }
There's only the Divide()
method interesting in the code,
throwing an exception if we divide by zero. The default C# .NET behavior for
decimal numbers would be to return the "Infinity" value, which always isn't what
the application user expects.
UnitTesting
In C# .NET, we write unit tests using tools from the
Microsoft.VisualStudio.TestTools.UnitTesting
namespace. Visual
Studio provides full support for these tests and we add them to our application
as another project to our solution. Like this, the tests are completely
separated from the project, which is a great design advantage, but we must not
forget to link the projects with the appropriate references.
In the Solution Explorer, we'll click the "CalculatorApp" solution with the right button and choose Add -> New Project...
The test project name is typically the application project name + the word "Tests", in our case, it's "CalculatorAppTests":
To the test project, we now need to add a reference to the application
project so we can access its classes. To do so, we'll right-click the
CalculatorAppTests
project and choose Add -> Reference ...
In the following form, we'll select the Projects -> Solution tab and check
the CalculatorApp
project. We'll confirm the dialog, making the
Calculator
class available.
In the CalculatorAppTest
project, a new UnitTest1
file was generated with the following code:
using System; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace CalculatorAppTests { [TestClass] public class UnitTest1 { [TestMethod] public void TestMethod1() { } } }
Perhaps you aren't surprised that, in object-oriented C#, class tests
(scenarios) are also represented by a class and the individual tests by methods
We've already encountered
attributes (square brackets above methods and classes) in the courses and we
know they provide some additional specification. [TestClass]
here
says the class represents a test scenario, the methods representing individual
tests are marked with [TestMethod]
(these will be invoked
automatically by Visual Studio). We'll rename the class (and its file) to
CalculatorTests
as it will contain tests for the
Calculator
class.
Class coverage with tests
We can use several more attributes in our unit tests. We'll use
[TestInitialize]
and also, for explanation purposes,
[TestCleanup]
, allowing us to mark the methods which will be called
before, respectively, after each test in this class. This is
very important to us because, according to best practices, we want our tests
to be independent. Usually, before each test, we prepare the
same environment again to avoid any interference of the tests with each other.
We'll discuss good practices in detail later. Let's add a
calculator
field to the class and, in the method with the
[TestInitialize]
annotation, we'll always create a new calculator
instance for each test. If there were any additional settings or dependencies
needed, they would be in this method as well. We'll remove the
TestMethod1()
method. For the calculator, we'll add
using CalculatorApp;
:
using System; using CalculatorApp; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace CalculatorAppTests { [TestClass] public class CalculatorTests { private Calculator calculator; [TestInitialize] public void Initialize() { calculator = new Calculator(); // Creates a new calculator before each test } [TestCleanup] public void Cleanup() { } }
We have everything ready to add tests, which we'll do in the next lesson, Testing in C# .NET - Finishing unit tests and best practices.