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
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>
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...
}
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);
}
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);
}
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)