DEV Community

Cover image for Introduction to Test Driven Development (TDD) and AAA Testing using xUnit
Sam Walpole
Sam Walpole

Posted on • Originally published at samwalpole.com

Introduction to Test Driven Development (TDD) and AAA Testing using xUnit

I'm not ashamed to admit that for a long time I avoided writing tests for my code. I saw it as something that took up precious time that I could have spent on "real" coding. However, after experiencing first-hand the headache caused from having to maintain production code with little-to-no tests, I've been convinced that writing tests really are worth your time. Too many times have I been bitten by hidden bugs, that could have easily been identified if the appropriate tests were in place.

Test Driven Development

Test Driven Development (TDD) is a software development cycle that focusses on describing the behaviour of your code first using tests, then implementing those behaviours. The advantage of this is that, once you have defined exactly how you expect you code to behave (including handling errors and edge cases), you can be confident that your implementation will actually handle all these behaviours properly, and it's less likely that bugs will creep in.

A typical TDD cycle is illustrated below. First you start by writing tests that test all of the behaviours that you wish to implement. At this point (since there is no implementation), all of the tests should fail. The next step is to write code that allows all of the tests to pass. At this stage don't worry about code quality, just get it to pass. Once all of the tests are passing, then you can go back and refactor the code to improve it's quality.

Test Driven Development Cycle

AAA Testing

AAA testing is a method for writing tests. It stands for Arrange, Act, Assert, and describes the basic method for setting up a test.

First, in the Arrange stage, you create all of the objects and variables that you require for your test. In the Act stage, you perform the behaviour that you wish to test. For example, this could be calling a particular method on your test object. Finally, in the Assert stage, you test that the final result (for example, object property values) are what you would expect if the behaviour had executed correctly.

A typical AAA test may look something like this:

[Fact]
public void Add_ArgValueOne_AddsOneToValue()
{
    // arrange
    var counter = new Counter(0);

    // act 
    counter.Add(1);

    // assert
    Assert.Equal(1, counter.Value);
}
Enter fullscreen mode Exit fullscreen mode

In this fictitious example, we have a Counter object that we Arrange to have a starting value of 0. We the Act by calling the Add method with an argument value of 1. Finally, we Assert that the final Value property is set to 1.

I also find this naming convention of naming test methods very useful for maintaining test code. As shown above, it consists of {Name of Method}_{Test Conditions}_{ExpectedResult}.

Testing using xUnit

xUnit is a popular testing framework for .NET applications. For these examples, I will be show snippets from code that I will be submitting to the upcoming Hashnode Hackathon. The GitHub repository is available here.

I'll start by talking a little bit about naming conventions. I like to have my test project mimic the structure of my source code projects, in terms of folder structure/namespaces etc. I implement a single test class per actual class. For example, my source code project is called WeKan.Domain and I have an Activity class in the WeKan.Domain.Activities namespace. Therefore, the equivalent test class is called ActivityTests and is in a project called WeKan.Domain.UnitTests and a namespace of WeKan.Domain.UnitTests.Activities.

The first step of TDD is to write the tests for your desired behaviour. However, that doesn't work if the classs you're testing doesn't exist or doesn't have the properties/methods that you wish to test. This seems like a bit of a paradox - you shouldn't write code before the tests, but you can't write tests without the code. What I have found to work well, is to create the class and define all of the properties and methods you require, but don't implement them. Instead, just have the methods throw a NotImplementedException so that the tests will fail. For example, this is what my Activity class looked like before I implemented any of it's behaviours.

public class Activity : Entity
{
    public static Activity Create(string title, int? order = null) => throw new NotImplementedException();

    private Activity(string title, int? order)
    {
        throw new NotImplementedException();
    }

    private Activity() { }

    public int CardId { get; private set; } // populated by EF Core

    public string Title { get; private set; } = string.Empty;

    public string Description { get; private set; } = string.Empty;

    public int? Order { get; private set; }

    public void TransferTo(Card card)
    {
        throw new NotImplementedException();
    }

    public void ChangeTitle(string title)
    {
        throw new NotImplementedException();
    }

    public void SetDescription(string description)
    {
        throw new NotImplementedException();
    }

    internal void ChangeOrder(int order)
    {
        throw new NotImplementedException();
    }
}
Enter fullscreen mode Exit fullscreen mode

An obvious test to start with is, if I try to instantiate the class with valid data, does it get instantiated with the expected values? Here is an example from ActivityTests:

[Fact]
public void Create_TitleLengthGreaterThanZero_ReturnsActivity()
{
    var title = "test-title";
    var activity = Activity.Create(title);

    Assert.IsType<Activity>(activity);
    Assert.NotNull(activity);
    Assert.Equal(title, activity.Title);
}
Enter fullscreen mode Exit fullscreen mode

With such a simple example, the Arrange and Act stages kind of get blended into one. You may notice that the method is annotated with the attribute [Fact]. This marks the method as a basic xUnit test (I will introduced another type later).

In xUnit, you make assertions using the Assert class, which has a multitude of different methods which should be sufficient to test almost any scenario.

In this example, I start by asserting that the activity that is created has the Activity type. Note that the IsType method checks that the instance has exactly that type. If you want to check if it belongs to a base class or interface, use the IsAssignableFrom method. Assert.NotNull check that the instance is not null. Finally, Assert.Equal tests the equality between an expected and actual value. I find that I end up using this method most often.

In my Activity class, I want it to throw an exception if the title argument is null or empty. Therefore, I'll create another test for this.

[Theory]
[InlineData(null)]
[InlineData("")]
public void Create_TitleNullOrEmpty_ThrowsArgumentException(string title)
{
    Activity action() => Activity.Create(title);

    Assert.Throws<ArgumentException>(action);
}
Enter fullscreen mode Exit fullscreen mode

A Theory is the second type of xUnit test. Theories allow you to pass arguments to the test method, in order to test using multiple different values. Here, the test will be ran twice, once with the title as null and then with the title as an empty string.

Testing for exceptions is also a bit different to other examples. You can't just run the code, since the test will fail if an exception is thrown. Therefore, you should define an inline function to perform the behaviour that you wish to test, and pass that function as an argument to the Assert.Throws method. This method tests that, first an exception is thrown, but also that the exception is of the given type (in this case ArgumentException).

Another interesting example is testing collections. Here, I have a Card class, which contains a collection of Activity objects. I want to test that, when the Card is created the collection is empty, and when I call the AddActivity method, the collection contains one Activity. For this, we can use the Assert.Empty and Assert.Single methods.

[Fact]
public void Create_ActivitiesCollectionIsEmpty()
{
    var title = "test-title";
    var card = Card.Create(title);

    Assert.NotNull(card.Activities);
    Assert.Empty(card.Activities);
}

[Fact]
public void AddActivity_ActivityInstance_AddsActivityToCollection()
{
    var activity = Activity.Create("activity-title");
    var title = "test-title";
    var card = Card.Create(title);

    card.AddActivity(activity);

    Assert.Single(card.Activities);
    Assert.Equal(activity, card.Activities.First());
}
Enter fullscreen mode Exit fullscreen mode

Finally, you may have noticed that the Activity method contains an internal method, ChangeOrder. If it's internal, how to we access it from our test project for testing? Fortunately, we can add an attribute to namespaces that allows internal properties/methods to be accessible to a different specified project. For example, in my DependencyInjection.cs (chosen simply because it sits at the root of the project namespace) I added the following attribute:

[assembly: InternalsVisibleTo("WeKan.Domain.UnitTests")]
namespace WeKan.Domain
{
    ...
}
Enter fullscreen mode Exit fullscreen mode

This states that any internal method/property/class within the WeKan.Domain namespace is also accessible from the WeKan.Domain.UnitTests namespace, meaning we can now test the internal methods.

Conclusion

Here I have introduced the concepts of Test Driven Development (TDD) and AAA (Arrange, Act, Assert) testing. I have then applied these principles to demonstrate the xUnit testing framework, a popular framework for .NET applications. I have covered some of the most common uses for the xUnit framework, as well as some common pitfalls (testing internal values). The GitHub repo containing these tests can be found here.

"Ten++ Ways to Make Money as a Developer" eBook

I post mostly about full stack .NET and Vue web development. To make sure that you don't miss out on any posts, please follow this blog and subscribe to my newsletter. If you found this post helpful, please like it and share it. You can also find me on Twitter.

Top comments (0)