Do you want to start writing unit tests and you don't know how to start? Were you asked to write some unit tests on a past interview? Let's see what is a unit test and how to write your first unit tests in C#.
What is a unit test?
The book The Art of Unit Testing defines a unit test as "an automated piece of code that invokes a unit of work in the system and then checks a single assumption about the behavior of that unit of work."
From the previous definition, a unit of work is any logic exposed through public methods. Often, a unit of work returns a value, changes the internals of the system, or makes an external invocation.
If that definition answers how to test public methods, we might ask: 'What about private methods?' Short answer: we don't test them. We test private methods when we call our code through its public methods.
In short, a unit test is code that invokes some code under test and verifies a given behavior of that code.
Why should we write unit tests?
Have you ever needed to change your code, but you were concerned about breaking something? I've been there too.
The main reason to write unit tests is to gain confidence. Unit tests allow us to make changes, with confidence that they will work. Unit tests allow change.
Unit tests work like a "safety net" to prevent us from breaking things when we add features or change our codebase.
In addition, unit tests work like a living documentation. The first end-user of our code is our unit tests. If we want to know what a library does, we should check its unit tests. Often, we will find not-documented features in the tests.
What makes a good unit test?
Now, we know what is a unit test and why we should write them. The next question we need to answer is: 'What makes a test a good unit test?' Let's see what all good unit tests have in common.
Our tests should run quickly. The longer our tests take to run, the less frequent we run them. And, if we don't run our tests often, we have doors opened to bugs.
Our tests should run in any order. Tests shouldn't depend on the output of previous tests to run. A test should create its own state and not rely upon the state of other tests.
Our tests should be deterministic. No matter how many times we run our tests, they should either fail or pass every time. We don't want our test to use random input, for example.
Our tests should validate themselves. We shouldn't debug our tests to make sure they passed or failed. Each test should determine the success or failure of the tested behavior. Imagine we have hundreds of tests and to make sure they pass, we have to debug every one of them. What's the point, then?
"It could be considered unprofessional to write code without tests" - Robert Martin, The Clean Coder
Let's write our first unit test
Let's write some unit tests for Stringie, a (fictional) library to manipulate strings with more readable methods.
One of Stringie methods is Remove()
. It removes chunks of text from an string. For example, Remove()
receives a substring to remove. Otherwise it returns an empty string if we don't pass any parameters.
"Hello, world!".Remove("Hello");
// ", world!"
"Hello, world!".Remove();
// ""
Here's the implementation of the Remove()
method for the scenario without parameters.
namespace Stringie
{
public static class RemoveExtensions
{
public static RemoveString Remove(this string source)
{
return new RemoveString(source);
}
}
public class RemoveString
{
private readonly string _source;
internal RemoveString(string source)
{
_source = source;
}
public static implicit operator string(RemoveString removeString)
{
return removeString.ToString();
}
public override string ToString()
{
return _source != null ? string.Empty : null;
}
}
}
Let's write some tests for the Remove()
method. We can write a Console program to test these two scenarios.
using Stringie;
using System;
namespace TestProject
{
class Program
{
static void Main(string[] args)
{
var helloRemoved = "Hello, world!".Remove("Hello");
if (helloRemoved == ", world!")
{
Console.WriteLine("Remove Hello OK");
}
else
{
Console.WriteLine($"Remove Hello failed. Expected: ', world!'. But it was: '{helloRemoved}'");
}
var empty = "Hello, world!".Remove();
if (string.IsNullOrEmpty(empty))
{
Console.WriteLine("Remove: OK");
}
else
{
Console.WriteLine($"Remove failed. Expected: ''. But it was: {empty}");
}
Console.ReadKey();
}
}
}
However, these aren't real unit tests. They run quickly, but they don't run in any order and they don't validate themselves.
Where should we put our tests?
Let's create a new project. Let's add to the solution containing Stringie a new project of type "MSTest Test Project (.NET Core)". Since we're adding tests for the Stringie project, let's name our new test project Stringie.UnitTests
.
It's my recommendation to put our unit tests in a test project named after the project they test. We can add the suffix "Tests" or "UnitTests". For example, if we have a library called MyLibrary
, we should name our test project: MyLibrary.UnitTests
.
In our new test project, let's add a reference to the Stringie project.
After adding the new test project, Visual Studio created a file UnitTest1.cs
. Let's rename it! We are adding tests for the Remove()
method, let's name this file: RemoveTests.cs
.
One way of making our tests easy to find and group is to put our unit tests separated in files named after the unit of work or entry point of the code we're testing. Let's add the suffix "Tests". For a class MyClass
, let's name our file: MyClassTests
.
MSTest
Now, let's see what's inside our RemoveTests.cs
file.
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Stringie.UnitTests
{
[TestClass]
public class RemoveTests
{
[TestMethod]
public void TestMethod1()
{
}
}
}
It contains one normal class and method. However, they're annotated with two unusual attributes: [TestClass]
and [TestMethod]
. These attributes tell Visual Studio that our file contains unit tests to run.
The [TestClass]
and [TestMethod]
attributes belong to a project called MSTest. Microsoft Test Framework (MSTest) is an open source unit testing framework. MSTest comes installed with Visual Studio.
Unit testing frameworks help us to write and run unit tests. Also, they create reports with the results of our tests. Other common unit testing frameworks include NUnit and XUnit.
How should we name our tests?
Let's replace the name TestMethod1
with a name that follows a naming convention.
We should use naming conventions to show the feature tested and the purpose behind of our tests. Tests names should tell what they're testing. A name like TestMethod1
doesn't say anything about the code under test and the expected result.
One naming convention for our test names uses a sentence to tell what they're testing. Often these names start with the prefix "ItShould" follow by an action. For our Remove()
method, it could be: "ItShouldRemoveASubstring" and "ItShouldReturnEmpty".
Another convention uses underscores to separate the unit of work, the test scenario and the expected behavior in our test names. If we follow this convention for our example tests, we name our tests: Remove_ASubstring_RemovesThatSubstring()
and Remove_NoParameters_ReturnsEmpty()
.
With this convention, we can read our test names out loud like this: "When calling Remove with a substring, then it removes that substring".
These names could look funny at first glance. We should use compact names in our code. However, when writing unit tests, readability is important. Every test should state the scenario under test and the expected result. We shouldn't worry about long test names.
Following the second naming convention, our tests look like this:
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Stringie.UnitTests
{
[TestClass]
public class RemoveTests
{
[TestMethod]
public void Remove_ASubstring_RemovesThatSubstring()
{
}
[TestMethod]
public void Remove_NoParameters_ReturnsEmpty()
{
}
}
}
How should we write our tests?
Now, let's write the body of our tests.
To write our tests, let's follow the Arrange/Act/Assert (AAA) principle. Each test should contain these three parts.
In the Arrange part, we create input values to call the entry point of the code under test.
In the Act part, we call the entry point to trigger the logic being tested.
In the Assert part, we verify the expected behavior of the code under test.
Let's use the AAA principle to replace one of our examples with a real test. Also, let's use line breaks to visually separate the AAA parts.
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Stringie.UnitTests
{
[TestClass]
public class RemoveTests
{
[TestMethod]
public void Remove_NoParameters_ReturnsEmpty()
{
string str = "Hello, world!";
string transformed = str.Remove();
Assert.AreEqual(0, transformed.Length);
}
}
}
We used the Assert
class from MSTest to write the Assert part of our test. This class contains methods like AreEqual()
, IsTrue()
and IsNull()
.
The AreEqual()
method checks if the result from a test is equal to an expected value. In our test, we used it to verified the length of the transformed string. We expect it to be zero.
Let's use a known value in the Assert part instead of repeating the logic under test in the assertions. It's OK to hardcode some expected values in our tests. We shouldn't repeat the logic under test in our assertions. For example, we can use well-named constants for our expected values.
Here's an example of how not to write the Assertion part of our second test.
[TestMethod]
public void Remove_ASubstring_RemovesThatSubstring()
{
string str = "Hello, world!";
string transformed = str.Remove("Hello");
var position = str.IndexOf("Hello");
var expected = str.Substring(position + 5);
Assert.AreEqual(expected, transformed);
}
Notice how it uses the Substring()
method in the Assert part to find the string without the Hello
substring. A better alternative is to use the expected result in the AreEqual()
method.
Let's rewrite our last test to use an expected value instead of repeating the logic being tested.
[TestMethod]
public void Remove_ASubstring_RemovesThatSubstring()
{
string str = "Hello, world!";
string transformed = str.Remove("Hello");
// Here we use the expected result ", world!"
Assert.AreEqual(", world!", transformed)
}
How can we run a test inside Visual Studio?
To run a test, let's right click on the [TestMethod]
attribute of the test and use "Run Test(s)". Visual Studio will compile your solution and run the test you clicked on.
After the test runs, let's go to the "Test Explorer" menu. There we will find the list of tests. A passed test has a green icon. If we don't have the "Test Explorer", we can use the "View" menu in Visual Studio and click "Test Explorer" to display it.
That's a passing test! Hurray!
If the result of a test isn't what was expected, the Assertion methods will throw an AssertFailedException
. This exception or any other unexpected exception flags a test as failed.
Cheatsheet
These are some of the most common Assertion methods in MSTest.
Method | Function |
---|---|
Assert.AreEqual | Check if the expected value is equal to the found value |
Assert.AreNotEqual | Check if the expected value isn't equal to the found value |
Assert.IsTrue | Check if the found value is true |
Assert.IsFalse | Check if the found value is false |
Assert.IsNull | Check if the found value is null |
Assert.IsNotNull | Check if the found value isn't null |
Assert.ThrowsException | Check if a method throws an exception |
Assert.ThrowsExceptionAsync | Check if an async method throws an exception |
StringAssert.Contains | Check if a found string contains a substring |
StringAssert.Matches | Check if a found string matches a regular expression |
StringAssert.DoesNotMatch | Check if a found string doesn't matches a regular expression |
CollectionAssert.AreEquivalent | Check if two collections contain the same elements |
CollectionAssert.AreNotEquivalent | Check if two collections don't contain the same elements |
CollectionAssert.Contains | Check if a collection contains an element |
CollectionAssert.DoesNotContain | Check if a collection doesn't contain an element |
Conclusion
Voilà! That's how you write your first unit tests in C# with MSTest. Don't forget to follow naming conventions and use the Assert
class when writing unit tests.
If you want to practice writing more test for Stringie, check my Unit Testing 101 repository over on GitHub.
In this repository, you will find two lessons. One lesson to write some unit tests and another lesson to fix some unit tests.
Happy testing!