The post “How to Unit Test: A Guide to a Clean Testing Environment” was more on the theoretical side explaining the needed mindset when evaluating all the parts required to set up a testing environment. This time we’ll take the hands-on approach and dive into unit testing. If the theory was what you were looking for, don’t fret, I’ll get you covered as well.
In this article we’ll focus on the following topics:
- What is unit testing
- Why even bother with unit tests as a dev
- Levels of testing
- Unit testing frameworks
- Dummy vs Fake vs Stub vs Mock vs Spy
- Clean Test Developer
Unit Testing Explained
If you’re reading this post, most likely, you already know what unit tests are. To have the same understanding of it, let me quickly explain what unit testing is:
Unit testing is usually performed by software developers to ensure that the code does what it’s expected to do, expected by the developer that writes the code. Let’s look at a very simple example to showcase unit testing.
Imagine having the following code:

There is already an implementation to check whether a file exists in the System.IO namespace and we’ll talk later about why one should not use it directly to implement checks but for now, let’s look at the piece of code in front of us.
There are 2 possible scenarios. Either the file exists, and everything is fine, or the file does not exist, and the method will throw an exception. And that’s what one would do as a dev, write 2 tests, one that ensures that in case the method “FileExists” returns true, the unit test will succeed as well, in addition, a second test will be added to ensure that the exception is raised in case the file does not exist.

And that’s basically what unit testing does, write tests, which ensure that the software behaves as excepted.
The purpose of unit testing
Why should you even bother with unit tests? Let’s start with a practical explanation to incentivize the unit testing and then get some theory in.
We had a simple method to ensure that a file exists. Now imagine that the implementation of FileSystemAccessProvider changed and a dev also modifies the code (picture code 1) according to the new changes to look something like that:

It might make sense in the new environment but it’s not the way the developer, that wrote the code and test initially, thought the code should work. The test will fail. And that’s awesome!
That’s exactly why we write the test. We want an early warning system if the code behaves not the way we expect it to for any reason. That’s one of the reasons what we want the unit test for.
But there is more. Analysis showed that defects that are found by the unit tests are only 4 times more expensive to fix compared to bugs that are detected while coding. In comparison, if a bug is found during the system test, the price is 10 times higher. So having good unit tests ensures that we can improve our quality without big monetary investments.

- Cheap and repeatable – bugs that are found during unit testing are the cheapest to fix. The ones that are found during coding are cheaper, yet I wouldn’t consider those to be bugs in the first place. Unit testing provides us with an early bug detection system.
- Improved code quality – Unit-testable Code is automatically cleaner. Bad code with lots of dependencies is hard to unit test, not saying that code with a lot of dependencies is always bad (it’s often the case) but that’s one of those “less is more” cases.In addition, when writing unit tests you’re put in a certain state of mind, you get a bit of a tester mindset, you think how you could break your code, what the corner-cases are, and so on.
- Easier to track down bugs – A bug that can be reproduced can also be fixed. Unit tests are the fastest way to get to the code you want to debug
- The better feeling during refactoring and changes – Every dev that ever worked on a legacy/brownfield project knows the dread of introducing changes to a highly critical piece of software. Unit tests (or rather tests in general but we try to focus here, so let’s stick to the unit tests) give you that warm and cozy feeling when you introduce a refactoring, and all the tests stay green.
- Even if they turn red, at least you know what you broke and can analyze and decide whether that’s an actual change or a bug that you’ve introduced.
Levels of testing

We focus on unit testing in this post but there are different kinds and levels of testing (unit testing, integration testing, acceptance testing, etc.). I’m not going to go into details on any of the others, but I wanted to show the boundaries of the responsibilities of every type of test. What’s important to see here is that unit tests are only responsible for testing individual components.
Unit Testing Frameworks
Selecting the correct tool for the task is very important. I’ll showcase two of the mainstream frameworks and two less-known ones that are suited to solving some specific problems that are not covered by most unit testing frameworks.
The commonly used frameworks are nUnit and xUnit, they are very similar, and you should be fine using any of them. As an old-school dev, I’m more used to using nUnit but I’ve also used xUnit, and switching from one to the other is rather easy. Let’s look at what makes them stand out.
![]() nUnit |
![]() xUnit |
---|---|
We saw in the graphic “levels of testing” that there is more than just unit testing. nUnit provides better support for integration and system testing. | Requires less annotating. No specific methods for SetUp and TearDown are needed. Those are the methods that are executed before and after a test and in the case of the xUnit constructor and dispose of methods are used for that. In the case of nUnit specific methods with [SetUp] and [TearDown], attributes must be used. |
nUnit has been around for a very long time, so the documentation is readily available. | Supported and used by Microsoft itself. It’s being updated more often and is always aligned with the .net Core / NET5 / NET6 changes. |
For any LINQ fanboy nUnit is the right tool as it supports fluent Assertions. | Has a strong community and a huge number of extensions that are tailored to cover missing features (like the fluent assertions that are some of the base nUnit features) |
Being developed slower than xUnit since it doesn’t have such a major player backing it up. | It is harder to find out-of-the-box solutions just by using google as there is not much documentation available. |
Some brownfield projects structures are not fully suited for unit testing just because of the way they were designed. When accessing some static methods or framework functions directly, it’s not possible to test those using the previously mentioned frameworks nUnit and xUnit.
Let’s look at the following code:

As you can see, we’re accessing a static property on the DateTime class (DateTime.Now). With the regular unit testing or mocking frameworks, it’s impossible to modify the result that will be provided by that class.
Usually, we would implement a TimeProvider : ITimeProvider and use that interface instead. Then the TimeProvider implementation can be substituted with a mock implementation. Which then allows us to provide back whichever time we want. We’ll look at that example later in more detail when discussing the different types of mocking.
I wanted to mention it now, as there are situations in which we’re unable to change the productive code and must test around it. I would not advise you in doing so, because that will lead to many other problems. But sometimes the decision is made on a level that is above us and we just must live with it.
The two frameworks I wanted to talk about are Moles, the isolation framework in .Net, which was developed by Microsoft, and the JustMock framework developed by Telerik. I wanted to write a lot more about them, but I found out that Moles are not supported by .net Core versions or NET5 and NET6, so, unfortunately, nothing to say there. But to my knowing JustMock does.
JustMock is neither free nor cheap, yet it allows you to mock anything, even static methods. If you end up in a situation in which you must write unit tests without the ability to avoid code constructs that access the static directly, try using it.
One more thing I want to point out about JustMock based on my personal experience. It is very dangerous.
It is dangerous in the sense that you can do ugly things with it. Especially once you dive into setups that replace static implementations. Be sure to document all of those otherwise you’ll run into problems on the build server. The tests will fail because the tests will start interacting with each other if you execute multiple tests in parallel.
4 + 1 Types of Mocking
There are 5 ways of mocking an input. Most of the time you’ll be using the Mocks, as they are the most complex type that allows using the features of the other 3. I say 3 because “Fake” is a type of its own.
Let’s look at the different mocking types using the method we saw in the picture “Code 1: simple method using the method we saw in the picture “Code 1: simple method”. What we’re working with is the FileSystemAccessProvider. We’ll look at what the implementation would look like for the first 3 and then use a framework to help us with the Mock.

Dummy: Just a placeholder to let the unit test pass when arguments must be passed over and used.

Stub: Fake data provider. Kind of like a bit more intelligent dummy.

Spy: Does not provide any information and rather observes and records how the class is being interacted with. For spy, we need a bit more sophisticated code. Like imagine having a logger that is used to record a user that was logged in. Something like that:

And the spy implementation would look like that. This allows us to spy on all the log messages that are being created by the TryLoginUser method.

Fake: Fake is a real implementation of a class that can’t be used in production. For example, if you think about a system that scans barcodes, you don’t want to have to scan those barcodes while testing the software, so instead, we write a fake class that just provides the barcodes based on the data defined in a text file.
Mock: Defines how it should be used and will raise an error if it’s used in an unspecified way. That’s the case when used strictly, otherwise, any calls that are not specified but are not required to return a response will be ignored.
Whenever I hear mocking, I automatically think about Moq. The best framework for handling the regular cases.
Let’s look at the method we already saw in the beginning under Code 1: simple method but this time at the whole class

As we can see the fileSystemAccessProvider is being passed in as a constructor parameter, we’re using dependency injection here, which then will allow us to replace it with a mock. As previously said, I’ll use nUnit but xUnit is pretty much the same from a syntax perspective.

There are a couple of things that are important here:
- We’re using the SetUp method to create the mock and the testee, which means that we’ll have new instances for every test
- The MockBehavior is set to Strict, which means that every call that is performed in the test needs to be defined, otherwise the mock will raise an exception. The alternative is MockBehavior.Loose, which just ignores any non-specified calls.
- The setup allows us to record our expected calls on the mock and the conditions that lead to the returned result. That all ends with us marking the setup as verifiable.
- Sometimes there is not much to assert on the test level, just as what we have in the first method, in such a case, we can verify whether the calls on the mock were performed, even if the MockBehavior is set to Loose.
- We use fluent assertions to check for the exception that is being raised.
Clean Test Developer
At the end let me provide some bullet points which lead to clean tests. Having clean tests is important because one must be able to easily understand what the developer that initially wrote the test was expecting from the code. It’s important to identify whether the newly introduced change was supposed to break the test or not.
- Separate your tests into 3 parts: Arrange, Act, Assert. That allows one to focus on one specific part of the test setup
- Use the same pattern for the test naming and make it describe what is being tested. I usually use the pattern of {ClassName}_{What is happening}_Then{What is expected to happen}
- Strict vs. Loose. When specifying the mock behavior, be clear in what you’re trying to achieve. I prefer using loose behavior so that not every change to my test method would break the test. That requires having good asserts in place thought.In general, it’s considered to be best to have the loose behavior for mocks that are less important, for example, one would set a logging mock to loose. And to have strict behavior for more important mocks where additional calls should be knowingly introduced
- Try to be precise in your assertions. Like when I assert for the specific exception type FileNotFoundException instead of using a generic one. Otherwise, you could end up in a scenario in which your test passes, yet the implementation is wrong. That is specifically true for exception asserts, otherwise, it could be a different exception being raised and the test still passes
- If you feel like it’s hard to write unit tests for some specific parts of the code, the code is probably not SOLID enough. Consider refactoring the code before writing the test
Besides unit testing, it’s always good to have End-to-End testing up and running automated. Not sure which tool to use? TestResults.io is always a good choice 😉 The only test automation tool that developers and testers ❤ love and QA Engineers, Test Analysts, Test Automation Engineers, Business Analysts, Management, …
Leave A Comment