Testing in .NET Core: Unit test, integration test
Tags: dotnet, testing, beginners, programming
Published: 2023-10-20 15:14:27

Hello everyone, today I would like to talk to you about a crucial phase during the development of a software application, framework, or product: **Testing**.

Implementing automated tests is essential to ensure the quality of our projects and to prevent any changes to our code from introducing unpleasant regressions.

We have at our disposal many types of **unit tests, integration tests**, and **end-to-end (E2E) tests**. In this article, we will examine the first two on this list and also mention a series of tools that can greatly assist you in creating your tests.

### Table of Contents:

- **[Test Pyramid](#test-pyramid)**
- **[NUnit for Unit Tests](#nunit-for-unit-tests)**
- **[NUnit for Integration Test](#nunit-for-integration-test)**

### Test Pyramid

The **Test Pyramid** is a concept that visualizes different types of **automated tests** by categorizing them based on their complexity and execution speed. The higher a test is on the pyramid, the slower it will be in overall execution.


![Test Pyramid](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/79xgpd3wpcvjby6hwpqe.png)

As you can see, starting from the bottom, we have **Unit Tests** at the base of the pyramid. They ensure that an isolated unit of code functions as it should and are the fastest tests in the pyramid.

At the second level of the pyramid, we encounter **Integration Tests**. These tests are used to verify if components interact correctly with each other to satisfy business rules. The main objective of these tests is to ensure that the involved components communicate without errors when combined.

These tests are much slower compared to Unit Tests and are typically used and **triggered** by a Continuous Integration (CI) system to ensure that the code being integrated does not conflict with other parts of the code.

At the top of our pyramid, we have **End-to-End (E2E)** tests, which verify the correct functioning of an application from the initial point to the final one, simulating a complete user journey. It is, of course, the slowest type of test in our pyramid.

### **Nunit for Unit Tests**

For this article, we will be using the **NUnit** library in the context of .NET Core. The reason I choose to use it is because I prefer it over **XUnit** (another testing library in the .NET context). NUnit has specific attributes to manage the test lifecycle, making our tests clear and clean (of course, like all things in software development, this is my opinion and perspective).

To provide an overview of the test lifecycle, we need to talk about the de facto standard for developing a test, which is the **3A paradigm (Arrange, Act, Assert)**. When developing a test, the test method follows these three common phases:

- **Arrange:** This is the phase in which we create all the objects we need to test classes, methods, or services that are under test.
- **Act:** It invokes the class, method, or service being tested.
- **Assert:** In this phase, we verify that the test has responded correctly using the Assert class's methods in NUnit (or in any testing library you are using).

> Remember that as a good testing practice, it's essential to test only one element at a time, be it a class, a method, or a service.
> 

Let's go through a small example of a Unit Test:

```csharp
using NUnit.Framework;

public class Calculator
{
    public int Sum(int a, int b)
    {
        return a + b;
    }
}

[TestFixture]
public class CalculatorTests
{
    [Test]
    public void TestSum()
    {
        // Arrange (Preparation)
        Calculator calculator = new Calculator();
        int number1 = 5;
        int number2 = 7;
        
        // Act (execution)
        int result = calcolatrice.Sum(number1, number2);
        
        // Assert (check)
        Assert.AreEqual(12, result);
    }
}
```

Now, starting from the base of this example, we can see that in the **Arrange** phase, we instantiated the **Calculator** class to subject the **Sum()** method to a test. However, the instance of that class will be available only in the **TestSum()** test method. But what if we had more methods to test?

If we had a **Multiply** method to test, we would have to repeat the entire arrangement process. This is where the NUnit lifecycle attributes I mentioned at the beginning of the session come into play. In this article, we'll cover four of them:

- **OneTimeSetup:** It activates only once at the beginning of all the tests in a class.
- **SetUp:** It activates just before the start of each test method.
- **OneTimeTearDown:** It activates only once at the end of all the tests in a class.
- **TearDown:** It activates immediately after the end of each test method.

Returning to the previous example:

```csharp

//file Calculator.cs
public class Calculator
{
    public int Sum(int a, int b)
    {
        return a + b;
    }
		public int Multiply(int a, int b)
		{
	      return a * b;
		}
}
// file CalculatorTests.cs
using NUnit.Framework;

[TestFixture]
public class CalculatorTests
{
		private Calcolatrice calculator;
		private int number1;
		private int number2;

		[OneTimeSetUp]
		public void OneTimeSetup()
		{
		    // Inizializzazione comune a tutti i test della classe
		    calculator = new Calculator();
		    numero1 = 5;
		    numero2 = 7;
		}

    [Test]
    public void TestSum()
    {        
        // Act (execution)
        int result = calculator.Sum(number1, number2);
        
        // Assert (check)
        Assert.AreEqual(12, result);
    }

		[Test]
		public void TestMultiply()
		{
		    // Act (execution)
		    int result = calculator.Multiply(number1, number1);
		
		    // Assert (check)
		    Assert.AreEqual(35, result);
		}
}
```

As you've seen in this example, we've centralized the creation of the instance of the **Calculator** class, making it available throughout the test class. The reason for preferring **NUnit** over **XUnit** (unless, of course, I'm required to use it for work-related reasons) is precisely the cleanliness that comes from these practices. If I needed to inject some dependencies into the test class constructor now, the *Arrange* phase would remain isolated, keeping the two phases distinct.

One very useful element I want to draw your attention to is the **TestCase** attribute, which allows us to define multiple test scenarios by retrieving values as if they were parameters passed to the test method:

```csharp
// file Calculator.cs
public class Calculator
{
    public int Sum(int a, int b)
    {
        return a + b;
    }
		public int Multiply(int a, int b)
		{
	      return a * b;
		}
}

// file CalculatorTests.cs
using NUnit.Framework;

[TestFixture]
public class CalculatorTests
{
		private Calcolatrice calculator;
		private int number1;
		private int number2;

		[OneTimeSetUp]
		public void OneTimeSetup()
		{
		    // Inizializzazione comune a tutti i test della classe
		    calculator = new Calculator();
		    numero1 = 5;
		    numero2 = 7;
		}

    [Test]
    public void TestSum()
    {        
        // Act (execution)
        int result = calculator.Sum(number1, number2);
        
        // Assert (check)
        Assert.AreEqual(12, result);
    }

		[Test]
		public void TestMultiply()
		{
		    // Act (execution)
		    int result = calculator.Multiply(number1, number1);
		
		    // Assert (check)
		    Assert.AreEqual(35, result);
		}
		
		[Test]
    [TestCase(2, 3, 6)]
    [TestCase(4, 6, 24)]
    [TestCase(0, 5, 0)]
    public void TestMultiplyWithMultipleCases(int a, int b, int expectedResult)
    {
        // Act (execution)
        int result = calculator.Multiply(a, b);

        // Assert (check)
        Assert.AreEqual(expectedResult, result);
    }
}
```

In this example, you can see that thanks to this, we have three test cases for the **TestMultiplyWithMultipleCases** method, where the first number in the **TestCase** attribute corresponds to the 'a' parameter, the second to 'b', and the third to 'expectedResult'.

In the last tests we executed, we always used scalar values as both parameters and results, and the methods under test had no internal dependencies. This made our testing much easier. But what if we had methods with dependencies?

Let's start by understanding what a dependency is. We refer to a dependency as a class, property, method, or object linked to another class, property, method, or object and essential for its proper functioning. For example, in the **Calculator** class:

```csharp
public class CalculatorScientific
{
    private ICalculator calculatorBase; // Calculator Dependence

    public CalculatorScientific(ICalculator calculator)
    {
        calculatorBase = calculator; // We initialize the dependency
    }

    public double Pow(int baseNumber, int sponent)
    {
        // We use the Basic Calculator to perform multiplication
        if (sponent== 0)
        {
            return 1;
        }
        else if (sponent> 0)
        {
            double result = 1;
            for (int i = 0; i < sponent; i++)
            {
                result = calculatorBase.Multiply(result, baseNumber);
            }
            return result;
        }
        else
        {
            throw new ArgumentException("The exponent must be a nonnegative number.");
        }
    }

    // More advanced methods here
}
```

Now, as you can see, the **ICalculator** interface has become a dependency of the **CalculatorScientific** class. So how do we test this class? Can we simply insert a **Calculator** object during the creation of an instance of **CalculatorScientific**? ****

```csharp
using NUnit.Framework;

[TestFixture]
public class CalculatorScientificTests
{
    private CalculatorScientific calculatorScientific;

    [OneTimeSetUp]
    public void OneTimeSetup()
    {
        Calculator calculatorBase = new Calculator();
        calculatorScientific = new CalculatorScientific(calculatorBase);
    }

    [Test]
    public void TestPow()
    {
        // Act
        double risultato = calcolatriceScientifica.Pow(2, 3);

        // Assert
        Assert.AreEqual(8, risultato);
    }
}
```

Now, this may not be the best approach because it creates a tight coupling between **Calculator** and **CalculatorScientific**, making it difficult to change the implementation of **Calculator** in the future or use a different class. This would require significant changes to **CalculatorScientific**.

Secondly, injecting real dependencies during a test might require access to external resources or behave unpredictably, like database connections and web services.

This is where **[Moq](https://github.com/devlooped/moq)** comes into play. It's a fantastic library that allows us to **mock** dependencies during the testing phase, isolating them and creating simulated implementations. Thanks to this library, we can have complete control over the configuration of dependency behaviors, making them return specific values or even generate exceptions.

In light of all this, let's rewrite our test:

```csharp
using Moq;
using NUnit.Framework;

[TestFixture]
public class CalculatorScientificTests
{
    private CalculatorScientific calculatorScientific;
    private Mock<Calculator> mockCalculator;

    [OneTimeSetUp]
    public void OneTimeSetup()
    {
        mockCalculator = new Mock<Calculator>();
        calculatorScientific = new CalculatorScientific(mockCalcolatrice.Object);
    }

    [Test]
    public void TestPotenza()
    {
        // Arrange
        mockCalculator.Setup(x => x.Multiply(It.IsAny<int>(), It.IsAny<int>())).Returns((int a, int b) => a * b);

        // Act
        double result = calculatorScientific.Pow(2, 3);

        // Assert
        Assert.AreEqual(8, result);
    }

    [Test]
    public void TestPotenza_ConEsponenteNegativo_DeveLanciareEccezione()
    {
        // Arrange
        mockCalculator.Setup(x => x.Multiply(It.IsAny<int>(), It.IsAny<int>())).Returns((int a, int b) => a * b);

        // Act & Assert
        Assert.Throws<ArgumentException>(() => calculatorScientific.Potenza(2, -3));
    }
}
```

In this example, we used Moq to create a mock of the **Calculator** class and configured its behavior using the **Setup** method to enable testing of the **Pow** method. The second method, on the other hand, checks that an **ArgumentException** is thrown when a negative exponent is used.

### **NUnit for Integration Test**

Now that we have extensively covered the part related to unit tests, we can focus on the second level of our pyramid, which is integration tests.

As mentioned earlier, this type of test is done to ensure the overall application's proper functioning, evaluating how different parts of the software interact and communicate in a more realistic environment, involving, for example, databases or external web services.

For this example, we will test a repository that queries data using Entity Framework Core. If you're not familiar with how to configure Entity Framework, here's a link to a guide I wrote: [The comprehensive guide to Entity Framework Core](https://dev.to/maurizio8788/the-comprehensive-guide-to-entity-framework-core-l5a).

Let's create our entity:

```csharp
public class Product
{
    public int ProductId { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}
```

Now let's define a repository for data access:

```csharp
public interface IProductRepository
{
    Product GetById(int productId);
    void Add(Product product);
    void SaveChanges();
}

public class ProductRepository : IProductRepository
{
    private ProductContext context;

    public ProductRepository(ProductContext context)
    {
        this.context = context;
    }

    public Product GetById(int productId)
    {
        return context.Products.Find(productId);
    }

    public void Add(Product product)
    {
        context.Products.Add(product);
    }

    public void SaveChanges()
    {
        context.SaveChanges();
    }
}
```

And let's create the test for our **ProductIntegrationTests** class:

```csharp
using System.Data.Entity;
using System.Linq;
using Moq;
using NUnit.Framework;

public class ProductIntegrationTests
{
    [Test]
    public void AddProductAndRetrieveFromDatabase()
    {
        // Arrange
        var product = new Product(){
					Name = "Test Product",
					Price = 10.00
				};

        var dbContextOptions = new DbContextOptionsBuilder<ProductContext>()
            .UseInMemoryDatabase(databaseName: "InMemoryDatabase")
            .Options;

        using (var context = new ProductContext(dbContextOptions))
        {
            var productRepository = new ProductRepository(context);
            productRepository.Add(product);
            productRepository.SaveChanges();
        }

        using (var context = new ProductContext(dbContextOptions))
        {
            var productRepository = new ProductRepository(context);

            // Act
            var retrievedProduct = productRepository.GetById(product.ProductId);

            // Assert
            Assert.IsNotNull(retrievedProduct);
            Assert.AreEqual(product.ProductId, retrievedProduct.ProductId);
            Assert.AreEqual(product.Name, retrievedProduct.Name);
            Assert.AreEqual(product.Price, retrievedProduct.Price);
        }
    }
}
```

In this example, we created a **Product** object, configured an **InMemoryDatabase** to simulate a database using Entity Framework Core, added the product to the database, and then retrieved it using the **GetById** method in the Act phase. We then created standard assertions, as we would in a typical Unit Test.

One last small piece of advice I would like to give you when performing integration tests is not to manually create your entity classes. Our example referred to a very small class with only two fields, but if you have much more complex objects, the task can become tedious and, moreover, it wouldn't allow us to focus on our true goal, which is testing.

For this purpose, I use a library called **[AutoFixture](https://autofixture.github.io/#)**, which allows us to create our test objects easily. It also lets us configure our objects using a builder, indicating which properties to create, which ones not to, the format, custom values, and more.

If we had used AutoFixture, its implementation would look like this:

```csharp
using System.Data.Entity;
using System.Linq;
using AutoFixture;
using Moq;
using NUnit.Framework;

public class ProductIntegrationTests
{
    [Test]
    public void AddProductAndRetrieveFromDatabase()
    {
        // Arrange
        var fixture = new Fixture();
        var product = fixture.Create<Product>();

        // other instructions...
    }
}
```

That's it! Isn't it fantastic?

Now, we've reached the end of this long article. I hope you enjoyed it and found it useful. If you did, please give it a like and share it as much as you can!

As always, I'd love to hear your suggestions and feedback to improve and delve deeper into these topics.

Happy Coding!