Code Review Videos > Learn C# > Writing C# Unit Tests In NUnit

Writing C# Unit Tests In NUnit

As a newbie to C# I knew I wanted to use Unit Tests as I do in every other programming language.

However, with three options to choose from:

  • MS Test
  • xUnit
  • NUnit

I was left unsure which one I should be using. Which one is more prominent on real world C# projects, for example?

Well, if you know the answer to that, please do leave a comment.

However, after some Googling and reading of various opinionated blogs, I ended up going with NUnit for reasons covered below.

Once I’d got over the initial hump of choosing a C# unit testing framework, it was time to dive in and actually start using it.

NUnit Test Setup in C#

In JavaScript / TypeScript there are generally two approaches to setting up your unit tests.

The first is to put your unit test files in the same directory as the implementations:

Another approach, one I see far less frequently, is to move all your tests into a top level directory called __tests__ and mirror the directory structure from src.

There are pros and cons to both.

I brought across this mindset to C#. And as I tend towards the former, I figured I would put my C# tests right next to the C# implementation files.

However, that’s not how things turned out.

What I’ve ended up with, and I don’t know if this is standard or not, is to have:

  • One C# Solution (my overriding project?)
  • A project for my unit tests
  • A project for my implementation

Essentially this is very similar to the second / __tests__ approach seen in JavaScript.

Oh, by the way, ignore the red squiggles for now. We will get to what is causing that in a short while.

As I say, I don’t know if this is common or not. How I got here is by following the Rider IDE prompts:

I right click on my top level project (my Solution), then Add > New Project…

From here, the thing I wanted to do was add in some tests. Well, what do you know? There’s the option to add in a new Unit Test Project.

And that just so happens to suggest using NUnit. Well, being a newb, I just accepted the suggested defaults.

Anyway, I quite like it.

Writing Tests In NUnit

Having written tests in various programming languages now, writing C# unit tests in NUnit felt very straightfoward.

By using the Rider prompt to add a Unit Test Project to my solution, I was automatically provided with a generated starting point:

using NUnit.Framework; namespace TestProject1; public class Tests { [SetUp] public void Setup() { } [Test] public void Test1() { Assert.Pass(); } }
Code language: C# (cs)

This should immediately be able to run and pass.

From there, there are a few places to go off exploring:

The overall look and feel of the test file is really similar to any other of the popular ‘unit’ based test libraries that I have used, such as JUnit and PHPUnit.

NUnit Makes For Concise Tests

My first use of NUnit was to solve a particular coding challenge that you can read about here.

Essentially we have a starting point in Gherkin syntax:

Scenario: Using reduce to sum numbers Given an array of zero elements When reduced Then the outcome should be zero Given an array of one number When reduced Then the outcome should be that same number Given an array of multiple numbers When reduced Then the outcome should be the sum of all numbers
Code language: Gherkin (gherkin)

And as we saw here, one possible way to convert this to unit tests in TypeScript would result in this:

describe("easy reduce", () => { describe("reduceNumbers", () => { describe("should return the sum of all numbers in an array", () => { test("zero element array", () => { const given: number[] = []; expect(reduceNumbers(given)).toEqual(0); }); test("one element array", () => { const given = [1]; expect(reduceNumbers(given)).toEqual(1); }); test("multiple element array", () => { const given = [1, 2, 3]; expect(reduceNumbers(given)).toEqual(6); }); }); }); });
Code language: TypeScript (typescript)

This is perhaps a little on the verbose side, but looks similar enough to how my real world unit testing tends to look in TypeScript projects.

We have a describe block for the top level concept.

Then a describe block for a particular function in the file.

Then sometimes a describe block for a concept in that function – maybe something like:

describe("for logged in users", () => {}); // and describe("for logged out users", () => {});
Code language: PHP (php)

And then inside the describe blocks we finally get to the test cases.

Whilst Jest does provide a functionality for writing multiple input values for a given test case, personally I find it doesn’t read particularly well. Instead I tend towards arrays of objects and forEach blocks. Something like this:

[ { description: "variant 1 description", a: 1, b: 2, expected: 3}, { description: "variant 2 description", a: 2, b: 1, expected: 3}, { description: "variant 3 description", a: 3, b: 0, expected: 3}, ].forEach( ({description, a, b, expected }) => { test(`add two numbers: ${description}`, () => { expect(myFunction(a, b)).toEqual(expected) }); });
Code language: JavaScript (javascript)

In real world scenarios, that tends to get quite messy, quite quickly. Also it’s a pain to have to comment out a lot of the test cases to isolate the one failure in the array.

Of course you could write out each test case by hand, as I have done in the example above with the multiple nested describes.

Trying to solve the first problem from the Gherkin syntax:

Given an array of zero elements When reduced Then the outcome should be zero
Code language: Gherkin (gherkin)

That’s actually harder than it looks for a C# newb.

An array of zero elements in JavaScript is really simple:

const empty = [];
Code language: JavaScript (javascript)

In TypeScript we start to see a problem:

But try to use it:

The issue here being:

TS7034: Variable 'empty' implicitly has type 'any[]' in some locations where its type cannot be determined. // pretty easy to solve btw const empty: number[] = []; const output = reduceNumbers(empty);
Code language: PHP (php)

TypeScript wants to know what this is an empty array of, which is a bit weird to the uninitiated.

We can solve it by explicitly telling TypeScript that we have an empty array of a certain type, in this case it is an empty array of numbers, or a number[] array.

Right, now take that concept into C# as a newb. No idea.

Let’s skip that then and work with the easier test case, which is that we start with an array of one number.

The Gherkin:

Given an array of one number When reduced Then the outcome should be that same number
Code language: Gherkin (gherkin)

And the C#:

[Test] public void TestReduceNumbers() { var input = new[] { 1 }; Assert.AreEqual(1,EasyReduce.ReduceNumbers(input)); }
Code language: C# (cs)

Sweet, that does pass.

C# was able to infer the type of out array (new []) by looking at the value(s) inside the array. In this case it’s a single integer so C# was able to determine / infer that we are using an int[].

TypeScript can infer this, too.

Anyway, next test case:

Given an array of multiple numbers When reduced Then the outcome should be the sum of all numbers
Code language: C# (cs)

It’s pretty easy to copy / paste, and expand out the first test case to meet this goal:

[Test] public void TestReduceNumbers() { var input = new[] { 1, 2, 3 }; Assert.AreEqual(6,EasyReduce.ReduceNumbers(input)); }
Code language: C# (cs)

But this creates out first real problem.

Our test name: TestReduceNumbers is duplicated from the previous test.

Where do we go with this?

  • TestReduceNumbers2 ?
  • TestReduceNumbersWithDifferentNumbers ?

Hmm. Not good.

This isn’t actually that big of a problem with just two test cases. But in the real world I know two cases are usually not enough. And so this is a problem that needs solving in a nicer way.

Refactoring To [TestCase]

This was the first big win I stumbled across whilst digging around in the NUnit docs.

The docs provides us with not one, but two potential improvements here.

The first? Use the [TestCase] attribute to provide multiple inputs to a single method:

[Test] [TestCase(new[] { 0 }, 0)] [TestCase(new[] { 1 }, 1)] [TestCase(new[] { 1, 2, 3 }, 6)] public void TestReduceNumbers(int[] input, int result) { Assert.AreEqual(result,EasyReduce.ReduceNumbers(input)); }
Code language: C# (cs)

Cool.

One test, multiple variations, works really nicely.

But there’s a second suggested improvement which makes this even nicer:

[Test] [TestCase(new[] { 0 }, ExpectedResult = 0)] [TestCase(new[] { 1 }, ExpectedResult = 1)] [TestCase(new[] { 1, 2, 3 }, ExpectedResult = 6)] public int TestReduceNumbers(int[] input) { return EasyReduce.ReduceNumbers(input); }
Code language: PHP (php)

Note there are a few tweaks here from the first approach.

The most subtle is that because we have now switched to a return on line 7, we must specify the return type of the function from void to int.

We have also dropped the second int result argument entirely, as we now use a named parameter of ExpectedResult in the [TestCase] definition, and then NUnit will use that against the return‘ed value.

Pretty neat. The one improvement I would have liked would be to put the ExpectedResult as the first argument, but that’s not possible in C# or we end up with an error: “Attribute arguments must precede property assignment“:

The last thing is to add in that first test case with the empty array. That isn’t something specific to NUnit, so I’ve deliberately kept it until last:

[Test] [TestCase(new int[] { }, ExpectedResult = 0)] [TestCase(new[] { 0 }, ExpectedResult = 0)] [TestCase(new[] { 1 }, ExpectedResult = 1)] [TestCase(new[] { 1, 2, 3 }, ExpectedResult = 6)] public int TestReduceNumbers(int[] input) { return EasyReduce.ReduceNumbers(input); }
Code language: C# (cs)

Essentially it’s the TypeScript thing again, with C# syntax. We must tell C# we meant an empty array of int.

This is slightly different again if using any array of string:

[Test] [TestCase(ExpectedResult = "")] [TestCase(new object[] { "" }, ExpectedResult = "")] [TestCase(new object[] { " " }, ExpectedResult = "")] [TestCase(new object[] { "aaa" }, ExpectedResult = "aaa")] [TestCase(new object[] { "aaa", "bbb", "ccc" }, ExpectedResult = "aaabbbccc")] public string TestReduceString(params string[] input) { return EasyReduce.ReduceString(input); }
Code language: C# (cs)

I honestly couldn’t tell you why we need the object[] not a string[], but it caught me out, so I’m putting it in. Note we always need to specify new object[] even when providing some initial string values, such as on lines 5 & 6.

Duplicate TestCaseAttribute Attribute

From the screenshots above it’s not hard to miss the red squiggles under the various TestCase attributes. But if you did miss them, here they are again, in close up and high definition:

The issue appears to be that Rider doesn’t like me duplicating the TestCase attribute in this way.

Why the error message thinks I’m using the TestCaseAttribute and not the TestCase attribute is confusing, but frankly the least of my problems at this point.

I’m guessing there’s a preferred way to provide multiple test cases.

But without a suggested fix and the fact that the official NUnit docs show the duplicated TestCase attribute usage leaves me wondering what I have done wrong here.

Next Steps

I really quickly learned a bunch about improving my NUnit tests just by skimming the docs and clicking on things that sounded interesting.

The two pages I found most useful as starting points are:

And then use the left hand menu to dive deeper into anything that looks potentially useful.

For Assertions in particular, the IDE intellisense autocomplete should get you most of the way. In Rider if you use an assertion such as Assert.Equals it will even tell you to make a change:

That should be enough to kick start your C# unit testing journey.

As ever there will undoubtedly be plenty more to learn, and whilst some of the attributes and assertions will be rarely used, it is nevertheless interesting to know about them and nice when you finally find that one esoteric use case for them.

Impress your colleagues, why not?

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: