DEV Community

Yousef
Yousef

Posted on

Unit Testing

This week, my focus in the development of Learn2Blog centred on implementing unit testing, a crucial aspect of ensuring the reliability and stability of the project. In this blog post, I'll delve into the significance of unit testing and touch upon the broader concept of end-to-end testing.

The Importance of Testing

Unit testing and end-to-end testing are essential practices in software development, contributing to the overall quality and robustness of a project. Unit testing involves testing individual components or functions in isolation, ensuring they produce the expected output. On the other hand, end-to-end testing validates the entire system's functionality, simulating real-world scenarios.

xUnit.net

I opted to use xUnit.net, a testing framework recommended by Microsoft's dotnet documentation. xUnit.net is known for its simplicity and efficiency in writing and executing tests.

To integrate xUnit.net into your project, execute the following command:

dotnet add package xunit
Enter fullscreen mode Exit fullscreen mode

For creating a testing project and class in Visual Studio (Code), consider adding the xunit.runner.visualstudio package, ensuring it's applied to the testing project, not the main one.

Creating a Testing Project

A testing project is crucial for maintaining a clean separation between the main project and its tests. In xUnit.net, each class and its methods in the testing project correspond to the classes and functionalities being tested.

Importing Main Project

To import the main project into the testing project, modify the TestingProject.csproj file:

<ItemGroup>
    <ProjectReference Include="Relative path to MainProject.csproj" />
</ItemGroup>
Enter fullscreen mode Exit fullscreen mode

Ensure the path is correctly specified.

Writing Tests

I organized my tests by creating separate test classes corresponding to classes in the main project. For instance, the main project class CommandLineParser has a corresponding test class named CommandLineParserTests.

Utilizing ITestOutputHelper allows printing messages for each test and we can use it in our test class like so:

public class CommandLineParserTests
{
    private readonly ITestOutputHelper output;

    public CommandLineParserTests(ITestOutputHelper output)
    {
        this.output = output;
    }

   // rest of the code...
}
Enter fullscreen mode Exit fullscreen mode

Here is an example of one of the tests the return outcome of running the app without any arguments:

[Fact]
public void TestNoArgumentReturnsNull()
{
    this.output.WriteLine("Should return null when user passes no arguments");

    var args = Array.Empty<string>();
    CommandLineOptions? options = CommandLineParser.ParseCommandLineArgs(args);

    Assert.Null(options);
}
Enter fullscreen mode Exit fullscreen mode

In this code, the [Fact] attribute indicates it as a test method. It then creates an empty array to pass as args to simulate passing no arguments through the CLI and then asserts that options is returned as null.

Using [Theory] in xUnit

xUnit.net's [Theory] attribute simplifies testing scenarios with different sets of input data.

[Theory]
[InlineData("-o")]
[InlineData("--output")]
public void TestOutputArgument(string arg)
{
    this.output.WriteLine("Should return option with OutputPath == 'testOutput'");

    string outputPath = "testOutput";
    var args = new string[] { arg, outputPath, "input" };
    CommandLineOptions? options = CommandLineParser.ParseCommandLineArgs(args);

    Assert.Equal(outputPath, options?.OutputPath);
}
Enter fullscreen mode Exit fullscreen mode

The [Theory] attribute allows running the same test with different input values. In this example, the test checks if the CommandLineParser correctly handles different forms of the output argument.

Incomplete Tests

While I successfully implemented several tests, some scenarios proved challenging. For example, testing the -c / --config argument requires mocking the config file, a task I documented in issue #18. Moq, a common tool for this in C# projects, was attempted but not fully successful.

Code Coverage

Code coverage is an essential metric indicating the percentage of code exercised by tests. Although I haven't yet implemented it in Learn2Blog, Microsoft guides generating code coverage reports for .NET projects here. The pursuit of 100% code coverage ensures a more comprehensive validation of code integrity.

End-to-End Testing

As of now, Learn2Blog lacks a dedicated end-to-end test. While core features have been extensively tested in unit tests, issue #20 has been created to address this gap.

End-to-end testing involves validating the entire application's workflow, ensuring all components work harmoniously. It complements unit testing by verifying the integration of various modules.

Lessons Learned

Reflecting on the testing process, it became evident that certain design flaws and bugs surfaced during testing. This emphasizes the importance of early test development, enabling the identification and rectification of issues before they escalate.

Automated tests, both unit and end-to-end, play a pivotal role in uncovering hidden bugs. The experience also highlighted the need for modular code, making it easier to test and maintain.

The bug with the StringWriter instance revealed during testing underscores the value of running multiple tests consecutively, mimicking real-world usage scenarios.

After squashing all of the different commits into a single one, it was merged into the main branch. You can review all the changes in 8e8edac.

Conclusion

In conclusion, the journey of implementing unit testing in Learn2Blog has been enlightening, exposing both strengths and areas for improvement. Embracing automated testing from the early stages is key to building a resilient and reliable software application. As development progresses, addressing the identified issues and continually expanding test coverage will be a priority for ensuring the long-term integrity of the project.

Top comments (0)