DEV Community

aliasadidev
aliasadidev

Posted on

C# Unit Testing

Overview

This guide is a basic introduction to Unit Testing in C#.

Introduction

Check that your code is working as expected by creating and running unit tests. It's called unit testing because you break down the functionality of your program into discrete testable behaviors that you can test as individual units. Unit Testing is a software testing approach which is performed at development time to test the smallest component of any software. Unit test cases help us to test and figure out whether the individual unit is performing the task in a good manner or not.

Why do we need the unit test?

One of the most valuable benefits of using Unit Tests for your development is that it may give you positive confidence that your code will work as you have expected it to work in your development process. Unit Tests always give you the certainty that it will lead to a long term development phase because with the help of unit tests you can easily know that your foundation code block is totally dependable on it.

There are a few reasons that can give you a basic understanding of why a developer needs to design and write out test cases to make sure major requirements of a module are being validated during testing:

  • Unit testing can increase confidence and certainty in changing and maintaining code in the development process.
  • Unit testing always has the ability to find problems in early stages in the development cycle.
  • Codes are more reusable, reliable and clean.
  • Development becomes faster.
  • Easy to automate.

Unit Test Rules

There are a lot of rules in unit testing that we have mentioned some important of them here:

1. Follow SOLID Principles:

SOLID principles provide us with ways to move from tightly coupled code and little encapsulation to the desired results of loosely coupled and encapsulated real needs of a business properly.

2. Make the code loosely coupled:

Loose coupling is preferred since through it changing one class will not affect another class. It reduces dependencies on a class. That would mean you can easily reuse it. Gets less affected by changes in other components.

public interface IUserRepository{
    User GetById(int);
}
public class UserRepository : IUserRepository{
    public User GetById(int) { 
    ...
    }
}

public class UserService {
    // use dependency injection to create your dependencies outside of the class
    public UserService(IUserRepository userRepository)
}
Enter fullscreen mode Exit fullscreen mode

3. Use IoC instantiation instead of hard-code instantiation (Avoid Tight coupling)

  • IoC instantiation (Loosely coupled)
[ApiController]
[Route("api/v1/tools")]
public class ToolsController : ControllerBase
{
    private readonly IToolService _toolService;
    public ToolsController(IToolService toolService)
    {
       _toolService = toolService ?? throw new ArgumentNullException(nameof(toolService));
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Hard-code instantiation (Tight coupling)
[ApiController]
[Route("api/v1/tools")]
public class ToolsController : ControllerBase
{
    private readonly IToolService _toolService;
    public ToolsController()
    {
       _toolService = new ToolService();
    }
}
Enter fullscreen mode Exit fullscreen mode

4. Use IOptions pattern instead of const variable

The options pattern uses classes to provide strongly typed access to groups of related settings. When configuration settings are isolated by scenario into separate classes, the app adheres to two important software engineering principles:

  • Encapsulation - Classes that depend on configuration settings depend only on the configuration settings that they use.
  • Separation of Concerns - Settings for different parts of the app aren't dependent or coupled to one another.

Use case:

  • Use Options pattern
// appsettings.json
{
"FilePath": {
        "LogFilePath": "./Logs"
    }
}
Enter fullscreen mode Exit fullscreen mode
public class FileSetting
{
    public string LogFilePath { get; set; }
}

public class ToolService : IToolService
{
    private readonly FileSetting _fileSetting;

    public ToolService(IOptions<FileSetting> fileSettingOption)
    {
        _fileSetting = fileSettingOption.Value ?? throw new ArgumentNullException(nameof(fileSettingOption));
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Use Const variable
public static class FileSetting {
    public const string LogFilePath = "./Logs" 
}
Enter fullscreen mode Exit fullscreen mode

5. Write Unit Test Cases only for small functionality

6. Unit test naming conventions

Test naming is important for teams on a long-term project as any other code style conventions. By applying code convention in tests you proclaim that each test name will be readable, and understandable and will have a well-known naming pattern for everyone on the project.

  • Unit test class name pattern
// Pattern : ClassName + Tests
// UserService class => UserServiceTests
// UserRepository class => UserRepositoryTests
Enter fullscreen mode Exit fullscreen mode
  • Unit test method name pattern
// Pattern : MethodName_StateUnderTest_ExpectedBehavior
// Method Name : GetById(id?)
// GetById_IdIsNotValid_ReturnsBadData
// GetById_UserNotFound_ReturnsItemNotFoundSr
// GetById_UserIdIsNull_ThrowsInvalidArgumentException
Enter fullscreen mode Exit fullscreen mode

7. If a function is performing so many operations then just write Unit Test Case for each individual function

public class UserService {
    public ServiceResponse<User> GetById(int id) {
        if( id == 0 ){
            return new BadDataSr<User>("id is not valid"); // The first unit test
        }
        var result = _userRepository(id);
        if(result == null) {
            return new ItemNotFoundSr<User>("user not found"); // The second unit test
        }
        // The third unit test
        return result;
    }
}

// We need to write three unit tests for the GetById method.
public class UserServiceTests {

[Fact]
void GetById_IdIsNotValid_ReturnsBadData(){}

[Fact]
void GetById_UserNotFound_ReturnsItemNotFoundSr(){}

[Fact]
void GetById_ValidData_ReturnsUser(){}

}
Enter fullscreen mode Exit fullscreen mode

8. Don't write Unit Test Cases which are dependent on another Unit Test Case

9. The name of the function for the Unit Test Case should be self-explanatory

10. Unit Test Cases should always be independent.

11. Performance-wise, Unit Test Case should always be fast

Arrange-Act-Assert Test Pattern:

Arrange-Act-Assert is a great way to structure test cases. It prescribes an order of operations:

  1. Arrange inputs and targets. Arrange steps should set up the test case. Does the test require any objects or special settings? Does it need to prep a database? Does it need to log into a web app? Handle all of these operations at the start of the test.
  2. Act on the target behavior. Act steps should cover the main thing to be tested. This could be calling a function or method, calling a REST API, or interacting with a web page. Keep actions focused on the target behavior.
  3. Assert expected outcomes. Act steps should elicit some sort of response. Assert steps verify the goodness or badness of that response. Sometimes, assertions are as simple as checking numeric or string values. Other times, they may require checking multiple facets of a system. Assertions will ultimately determine if the test passes or fails.

Example

[Fact]
void GetById_IdIsNotValid_ReturnsBadData()
{

// Arrange
var userService = new UserService();
int id = 0;

// Act
var response = userService.Get(id);

// Assert
Assert.Equal("id is not valid", response.Message);
}
Enter fullscreen mode Exit fullscreen mode

Code coverage tooling

Unit tests help to ensure functionality and provide a means of verification for refactoring efforts. Code coverage is a measurement of the amount of code that is run by unit tests - either lines, branches, or methods.

 # https://github.com/danielpalme/ReportGenerator
 dotnet tool install -g dotnet-reportgenerator-globaltool

 dotnet test --collect:"XPlat Code Coverage" 

 reportgenerator -reports:"\TestResults\**\coverage.cobertura.xml" -targetdir:"coveragereport" -reporttypes:Html
Enter fullscreen mode Exit fullscreen mode

Top comments (0)