Writing Unit Tests
#
IntroductionUnit tests are used to test the functionality of an individual function or a collection of functions. This will be a simple introduction to unit testing, where we use TDD to implement a basic greeter method.
This article assumes you are working in a repository forked from LBHackney-IT/lbh-base-api.
#
Video TutorialWatch the video version of this page if you prefer:
#
Create the Test FileCreate a new file somewhere, named GreetingGatewayTests.cs
.
- The
using
statements at the top provide references to things we will use later on. - At HackIT, we typically use a separate
<ProjectName>.Tests
namespace to contain test code.
This doesn't test anything yet! We need to add some more code.
Create the test class, and the initial signature of the test method.
- Two of the lines above are highlighted. These
[Symbols]
are called Attributes; they are part of the C# Language.
We are using the NUnit unit-testing framework in this
example, so we use [TestFixture]
attribute to indicate to NUnit that we are
writing a test class; and [Test]
to indicate that we are writing a test
method.
Also, notice the test method name. The name should be descriptive and indicative of the desired effect of calling the method.
Now, add the actual test code!
First, we call the method we are testing, and assign its result to the
variable result
.
Then, we compare the result we captured with the result we want the method to
provide.
- The
Should().BeEquivalentTo()
syntax is provided byFluentAssertions
extension methods. It makes our Assert lines read more nicely. - Your IDE or Text Editor might complain when you write these lines, because
neither the
GreetingGateway
nor itsGetGreetingForName
method exists yet. This is normal, and is actually part of the TDD process!
#
Running the TestNow that we have some test code, we can try to run it.
- Running
dotnet test
this time exited before any tests were run, so the output here is a compilation error, rather than being from the testing framework itself. However, we can treat this as a failing test, and solve the problem that the output describes.
The output tells us that GreetingGateway
doesn't exist. That's true - we
haven't made it yet!
#
Create the ImplementationWe will write the most minimal piece of code that will pass the test.
- We are using the same names for the class and method that we wrote into the test method.
#
Running the testWe have addressed the error in the previous test run's output by creating the
class that didn't exist, GreetingGateway
(and added the method pre-emptively,
to save some test runs).
All of the tests in the project have passed! Our new code works, and it doesn't break anything that already existed in the codebase. Good news?
At this point, it probably seems like the method is wrong. It only works for James, and it should greet anyone. In reality, though, the method is fine - it's the test that is wrong!
#
Refactor the TestWe need to make sure that this greeting method works regardless of the name of the person it is greeting. Since this is a requirement of the feature, the test should enforce it.
What we want to do, is pass a name into GetGreetingForName
, and have it return
an appropriate greeting for that name.
- We've used string
interpolation
in the Assert, so that whatever we set
name
to, will be what we test theresult
for.
#
Running the testsAnd we are back to a compilation error.
This time, it's because the method we've written for the implementation doesn't take any arguments, but we are trying to use one in the test! Let's fix that.
Now the method takes the name to be greeted as an argument.
We are still getting a compilation error, because the compiler mandates that we use the parameter we added to the method.
This can be done using exactly the same interpolation used in the test itself.
Check the tests again:
This passes! ๐
#
Refactoring the Test (Again)To be pedantic, the test we currently have only ensures that the method will work for people named Lisa. This is a simple example, so we can see quite clearly that this would work for other names as well.
In other scenarios, it makes sense to test more thoroughly, covering as many use cases as possible. One approach that can help with this is to generate lots of different inputs to the method, and check that they all work. This can help uncover strange edge cases.
We can use a library called Bogus to help with this.
In this example, we use Bogus to pick a random first name to assign to the
name
variable. Every time the test is run, we assert that the method runs for
the random name.
Everything is still passing! The feature is complete and we have a robust test for it.