This article discusses Test-Driven Development in C# through a simple example.
Contents
- Introduction
- Development Environment
- Prerequisites
- Fake it!
- Triangulation
- Multiple Translations
- Reverse Translation
- File Loading
- TranslatorDataSourceTest
- TranslatorParserTest
- TranslatorLoaderTest
- Class Diagram
- Test Results
- Code Coverage
- Running the Source Code
- Is TDD a Time Waster?
- Conclusion
- History
The traditional approach to writing unit tests involves writing tests to check the validity of your code. First, you begin by writing the code and then you write the tests. This is the opposite of test-driven development.
Test-driven development (TDD) involves writing tests before writing code, as shown in the workflow above.
First, the test is written and must fail at the beginning. Then, we write the code so that the test passes. Then, the test must be executed and must succeed. Then, the code is refactored. Then, the test must be performed again to ensure that the code is correct.
To summarize, this is done in five steps:
- Write a test.
- The test must fail at the beginning.
- Write the code so that the test passes.
- Execute the test and make sure it passes.
- Refactor the code.
We can notice that in the workflow explained above, the tests are executed after the code has been refactored. This ensures that the code remains correct after refactoring.
This article will discuss TDD in C# through a simple example. The purpose of the example is to describe each step of TDD. The example will be developed in C# and the testing framework used is xUnit. We will be using Moq for mocking and dotCover for code coverage. We will be creating a multilingual translator through TDD. When writing code, we will try to respect SOLID principles and achieve 100% code coverage.
- Visual Studio 2022 >= 17.8.0
- .NET 8.0
The first task to achieve when using TDD is important: it must be so simple that the loop red-green-refactor can be completed quickly.
We first create a test class called TranslatorTest
:
public class TranslatorTest
{
}
Then we will create the first unit test in this class, we initialize an object of type Translator
with the name "en-fr"
and we will check if the name is correct:
public class TranslatorTest
{
[Fact]
public void TestTranslatorName()
{
var translator = new Translator("en-fr");
Assert.Equal("en-fr", translator.Name);
}
}
The test will fail, which is what we want.
Now that we have reached the red bar, we will write some code so that the test passes. There are many ways to do this. We will use the "Fake it!
" method. Specifically, it includes the minimum required to pass the test. In our case, it is enough to write a Translator
class that returns the property Name of "en-fr"
:
public class Translator
{
public string Name => "en-fr";
}
We will create the code step by step by using simple and quick methods in each step. Now, if we run our unit test again, it will pass. But the code is not refactored. There is a redundancy. Indeed, "en-fr"
is repeated twice. We will refactor the code:
public class Translator(string name)
{
public string Name => name;
}
After refactoring the code, we have to run the tests again to make sure the code is correct.
Code refactoring is a form of modifying code that preserves the execution of existing tests and obtains a software architecture with minimal defects. Some examples:
- Remove duplicate code / move code.
- Adjust
private
/ public
properties / methods.
We noticed that we have completed the cycle of TDD workflow. Now we can start the cycle over and over again with new tests.
In TDD, we write tests first, generating functional requirements before coding requirements. To refine the test, we will apply triangulation method.
Let's write a test that checks if a translation has been added to the translator (AddTranslation
).
Testing will be done through GetTranslation
method.
[Fact]
public void TestOneTranslation()
{
var translator = new Translator("en-fr");
translator.AddTranslation("against", "contre");
Assert.Equal("contre", translator.GetTranslation("against"));
}
If we run the test, we'll notice that it fails. Okay, so that's what we're looking for at this step.
First, we'll use the "Fake it!" method to pass the test:
public class Translator(string name)
{
public string Name => name;
public void AddTranslation(string word, string translation)
{
}
public string GetTranslation(string word) => "contre";
}
After running the test TestOneTranslation
, we will notice that it passes.
But wait, there's code duplication. The keyword "contre"
is repeated twice in the code. We will change the code to remove this duplication:
public class Translator(string name)
{
private readonly Dictionary<string, string> _translations = new();
public string Name => name;
public void AddTranslation(string word, string translation)
{
_translations.Add(word, translation);
}
public string GetTranslation(string word) => _translations[word];
}
After refactoring the code, we have to run the tests again to make sure the code is correct.
Let's add a test to check if the translator is empty:
[Fact]
public void TestIsEmpty()
{
var translator = new Translator("en-fr");
Assert.True(translator.IsEmpty());
}
If we run the test, we'll notice that it fails. Okay, so that's what we're looking for at this step. Let's use the "Fake it!" method and write some code to pass the test:
public class Translator(string name)
{
[...]
public bool IsEmpty() => true;
}
If we run the test, we'll notice that it passes.
Now, let's use triangulation technique by using two assertions to drive the generalization of the code:
[Fact]
public void TestIsEmpty()
{
var translator = new Translator("en-fr");
Assert.True(translator.IsEmpty());
translator.AddTranslation("against", "contre");
Assert.False(translator.IsEmpty());
}
Now if we run the test again, It will fail because of the second assertion. This is called triangulation.
So let's fix this:
public class Translator(string name)
{
[...]
public bool IsEmpty() => _translations.Count == 0;
}
If we run the test again, we'll notice that it passes.
One feature of the translator is the ability to manipulate multiple translations. This use case was not initially planned in our architecture. Let’s write the test first:
[Fact]
public void TestMultipleTranslations()
{
var translator = new Translator("en-fr");
translator.AddTranslation("against", "contre");
translator.AddTranslation("against", "versus");
Assert.Equal<string[]>(["contre", "versus"],
translator.GetMultipleTranslations("against"));
}
If we run the test, we'll notice that it fails. Okay, so that's what we're looking for at this step. First, we will use the "Fake it!" method to pass the test by modifying the method AddTranslation
and adding the GetMultipleTranslations
method:
public class Translator(string name)
{
private readonly Dictionary<string, List<string>> _translations = new();
public string Name => name;
public string[] GetMultipleTranslation(string word) => ["contre", "versus"];
[...]
}
After running the test TestMultipleTranslations
, we will notice that it passes. But wait, there's code duplication. The string array ["contre", "versus"]
is repeated twice in the code. We will change the code to remove this duplication:
public class Translator(string name)
{
private readonly Dictionary<string, List<string>> _translations = new();
public string Name => name;
public void AddTranslation(string word, string translation)
{
if (_translations.TryGetValue(word, out var translations))
{
translations.Add(translation);
}
else
{
_translations.Add(word, [translation]);
}
}
public string[] GetMultipleTranslation(string word) => [.. _translations[word]];
public string GetTranslation(string word) => _translations[word][0];
public bool IsEmpty() => _translations.Count == 0;
}
If we run the test again, we'll notice that it passes. Let's do some refactoring and rename GetMultipleTranslations
to GetTranslation
:
public class Translator(string name)
{
private readonly Dictionary<string, List<string>> _translations = new();
public string Name => name;
public void AddTranslation(string word, string translation)
{
if (_translations.TryGetValue(word, out var translations))
{
translations.Add(translation);
}
else
{
_translations.Add(word, [translation]);
}
}
public string[] GetTranslation(string word) => [.. _translations[word]];
public bool IsEmpty() => _translations.Count == 0;
}
We also have to change our tests:
public class TranslatorTest
{
[Fact]
public void TestTranslatorName()
{
var translator = new Translator("en-fr");
Assert.Equal("en-fr", translator.Name);
}
[Fact]
public void TestIsEmpty()
{
var translator = new Translator("en-fr");
Assert.True(translator.IsEmpty());
translator.AddTranslation("against", "contre");
Assert.False(translator.IsEmpty());
}
[Fact]
public void TestOneTranslation()
{
var translator = new Translator("en-fr");
translator.AddTranslation("against", "contre");
Assert.Equal<string[]>(["contre"], translator.GetTranslation("against"));
}
[Fact]
public void TestMultipleTranslations()
{
var translator = new Translator("en-fr");
translator.AddTranslation("against", "contre");
translator.AddTranslation("against", "versus");
Assert.Equal<string[]>(["contre", "versus"], translator.GetTranslation("against"));
}
}
Now suppose we want to consider translation in both directions, for example, a bilingual translator. Let's create the test first:
[Fact]
public void TestReverseTranslation()
{
var translator = new Translator("en-fr");
translator.AddTranslation("against", "contre");
Assert.Equal<string[]>(["against"], translator.GetTranslation("contre"));
}
If we run the test, we'll notice that it fails. Okay, so that's what we're looking for at this step. Now, let's write the code to pass the test by using the "Fake it!" method:
public string[] GetTranslation(string word)
{
if (_translations.TryGetValue(word, out var translations))
{
return [.. translations];
}
return ["against"];
}
The test will pass. But there is a code duplication. Indeed, "against"
is repeated twice. So let's refactor the code:
public string[] GetTranslation(string word)
{
if (_translations.TryGetValue(word, out var translations))
{
return [.. translations];
}
return [.. from t in _translations
where t.Value.Contains(word)
select t.Key];
}
If we run the test again, we'll notice that it passes.
Now, let's handle loading translations from a data source (such as an external text file). Let's focus on external text files for now. The input format will be a text file where the first line contains the name of the translator and the other lines contain words separated by " = "
. Here is an example:
en-fr
against = contre
against = versus
Here is the list of tests we will perform:
- Empty file.
- File containing only translator name.
- File with translations.
- Wrong file.
First, we'll use mocks to write tests. Then we'll write code along the way. Then we'll refactor the code. Finally, we'll test the code to make sure we refactored correctly and everything is working properly. We will create three new test classes:
TranslatorDataSourceTest
: We will test a translator loaded from an external data source. TranslatorParserTest
: We will test the parsing of loaded translator data. TranslatorLoaderTest
: We will test the loading of translator data loaded from an external data source.
Empty Translator Name
First, let's write the test:
[Fact]
public void TestEmptyTranslatorName()
{
var mockTranslatorParser = new Mock<ITranslatorParser>();
mockTranslatorParser
.Setup(dp => dp.GetName())
.Returns(string.Empty);
var translator = new Translator(mockTranslatorParser.Object);
Assert.Equal(string.Empty, translator.Name);
}
The test will fail. Okay, so that's what we're looking for at this step. We will use the interface ITranslatorParser
to parse translator data loaded from external data source. Following is the interface ITranslatorParser
:
public interface ITranslatorParser
{
string GetName();
}
Let's modify the Translator
class using the "Fake it!" method to pass the test:
public class Translator
{
private readonly Dictionary<string, List<string>> _translations;
public string Name { get; private set; }
public Translator(string name)
{
_translations = [];
Name = name;
}
public Translator(ITranslatorParser parser)
{
Name = string.Empty;
}
[...]
}
If we run the test again, we'll notice that it passes. But wait, there's a duplication in the code. Indeed, string.Empty
is repeated twice. So, let's do some refactoring:
public class Translator
{
private readonly Dictionary<string, List<string>> _translations;
public string Name { get; private set; }
public Translator(string name)
{
_translations = [];
Name = name;
}
public Translator(ITranslatorParser parser)
{
Name = parser.GetName();
}
}
If we run the test again, we'll notice that it passes.
No Translation
First, let's start by writing a test:
[Fact]
public void TestEmptyFile()
{
var mockTranslatorParser = new Mock<ITranslatorParser>();
mockTranslatorParser
.Setup(dp => dp.GetTranslations())
.Returns([]);
var translator = new Translator(mockTranslatorParser.Object);
Assert.Equal([], translator.GetTranslation("against"));
}
We will notice that the test will fail. Okay, so that's what we're looking for at this step. First, let's modify the interface ITranslatorParser
:
public interface ITranslatorParser
{
string GetName();
Dictionary<string, List<string>> GetTranslations();
}
Then, let's write some code to pass the test using the "Fake it!" method:
public class Translator
{
private readonly Dictionary<string, List<string>> _translations;
public string Name { get; private set; }
public Translator(string name)
{
_translations = [];
Name = name;
}
public Translator(ITranslatorParser parser)
{
_translations = [];
Name = parser.GetName();
}
[...]
}
If we run the test again, we'll notice that it passes. But wait, there's a duplication in the code. In fact, the translator initialization is repeated twice. So, let's do some refactoring:
public class Translator
{
private readonly Dictionary<string, List<string>> _translations;
public string Name { get; private set; }
public Translator(string name)
{
_translations = [];
Name = name;
}
public Translator(ITranslatorParser parser)
{
_translations = parser.GetTranslations();
Name = parser.GetName();
}
[...]
}
If we run the test again, we'll notice that it passes.
File with only Translator Name
First, let's start by writing a test:
[Fact]
public void TestTranslatorName()
{
var mockTranslatorParser = new Mock<ITranslatorParser>();
mockTranslatorParser
.Setup(dp => dp.GetName())
.Returns("en-fr");
var translator = new Translator(mockTranslatorParser.Object);
Assert.Equal("en-fr", translator.Name);
}
We will notice that the test will pass because we have written the interface ITranslatorParser
and changed the Translator
class. Currently, this unit does not require refactoring.
One Translation
First, let's start by writing a test:
[Fact]
public void TestOneTranslation()
{
var mockTranslatorParser = new Mock<ITranslatorParser>();
mockTranslatorParser
.Setup(dp => dp.GetTranslations())
.Returns(new Dictionary<string, List<string>>
{
{"against", ["contre"] }
});
var translator = new Translator(mockTranslatorParser.Object);
Assert.Equal<string[]>(["contre"], translator.GetTranslation("against"));
}
We will notice that the test will pass because we have written the interface ITranslatorParser
and changed the Translator
class. Currently, this unit does not require refactoring.
Multiple Translations
First, let's start by writing a test:
[Fact]
public void TestMultipleTranslations()
{
var mockTranslatorParser = new Mock<ITranslatorParser>();
mockTranslatorParser
.Setup(dp => dp.GetTranslations())
.Returns(new Dictionary<string, List<string>>
{
{ "against", ["contre", "versus"]}
});
var translator = new Translator(mockTranslatorParser.Object);
Assert.Equal<string[]>(["contre", "versus"], translator.GetTranslation("against"));
}
We will notice that the test will pass because we have written the interface ITranslatorParser
and changed the Translator
class. Currently, this unit does not require refactoring.
Wrong File
First, let's start by writing a test:
[Fact]
public void TestErroneousFile()
{
var mockTranslatorParser = new Mock<ITranslatorParser>();
mockTranslatorParser
.Setup(dp => dp.GetTranslations())
.Throws(new TranslatorException("The file is erroneous."));
Assert.Throws<TranslatorException>(() => new Translator(mockTranslatorParser.Object));
}
We will notice that the test will pass because we have written the interface ITranslatorParser
and changed the Translator
class. Currently, this unit does not require refactoring.
Now let's create a class to parse loaded translator data through ITranslatorLoader
, which loads translator data from external data source.
Empty Translator Name
First, let's start by writing a test:
[Fact]
public void TestEmptyTranslatorName()
{
var mockTranslatorLoader = new Mock<ITranslatorLoader>();
mockTranslatorLoader
.Setup(dl => dl.GetLines())
.Returns([]);
var translatorParser = new TranslatorParser(mockTranslatorLoader.Object);
Assert.Equal(string.Empty, translatorParser.GetName());
}
The test will fail. Okay, so that's what we're looking for at this step. We will use an interface ITranslatorLoader
to load translator data from an external data source. The following is the interface ITranslatorLoader
:
public interface ITranslatorLoader
{
string[] GetLines();
}
Let's write some code to pass the test using the "Fake it!" method:
public class TranslatorParser(ITranslatorLoader loader) : ITranslatorParser
{
public string GetName() => string.Empty;
public Dictionary<string, List<string>> GetTranslations() => new();
}
The test will pass. Let's move on to other units.
No Translation
Let's start by writing a test:
[Fact]
public void TestNoTranslation()
{
var mockTranslatorLoader = new Mock<ITranslatorLoader>();
mockTranslatorLoader
.Setup(dl => dl.GetLines())
.Returns([]);
var translatorParser = new TranslatorParser(mockTranslatorLoader.Object);
Assert.Equal([], translatorParser.GetTranslations());
}
The test will pass. Let's move on to other units.
Translator Name
Let's start by writing a test:
[Fact]
public void TestTranslatorName()
{
var mockTranslatorLoader = new Mock<ITranslatorLoader>();
mockTranslatorLoader
.Setup(dl => dl.GetLines())
.Returns(["en-fr"]);
var translatorParser = new TranslatorParser(mockTranslatorLoader.Object);
Assert.Equal("en-fr", translatorParser.GetName());
}
The test will fail. Okay, so that's what we're looking for in this step. Let's write some code to pass the test using the "Fake it!" method:
public class TranslatorParser(ITranslatorLoader loader) : ITranslatorParser
{
public string GetName() => "en-fr";
public Dictionary<string, List<string>> GetTranslations() => new();
}
The test will pass. But wait, there is a duplication in the code and the test TestEmptyTranslatorName
fails. So let's solve this issue:
public class TranslatorParser(ITranslatorLoader loader) : ITranslatorParser
{
private readonly string[] _lines = loader.GetLines();
public string GetName() => _lines.Length > 0 ? _lines[0] : string.Empty;
public Dictionary<string, List<string>> GetTranslations() => new();
}
Now, the test will pass.
One Translation
Let's start by writing a test:
[Fact]
public void TestOneTranslation()
{
var mockTranslatorLoader = new Mock<ITranslatorLoader>();
mockTranslatorLoader
.Setup(dl => dl.GetLines())
.Returns(["en-fr", "against = contre"]);
var translatorParser = new TranslatorParser(mockTranslatorLoader.Object);
var expected = new Dictionary<string, List<string>>
{
{"against", ["contre"]}
};
Assert.Equal(expected, translatorParser.GetTranslations());
}
The test will fail. Okay, so that's what we're looking for at this step. Let's write some code to pass the test using the "Fake it!" method:
public class TranslatorParser(ITranslatorLoader loader) : ITranslatorParser
{
private readonly string[] _lines = loader.GetLines();
public string GetName() => _lines.Length > 0 ? _lines[0] : string.Empty;
public Dictionary<string, List<string>> GetTranslations() => new()
{
{"against", ["contre"]}
};
}
The test will pass. But wait, there is a duplication in the code and the test TestNoTranslation
fails. So let's fix this:
public partial class TranslatorParser(ITranslatorLoader loader) : ITranslatorParser
{
private static readonly Regex TranslatorRegex = new(@"^(?<key>\w+) = (?<value>\w+)$");
private readonly string[] _lines = loader.GetLines();
public string GetName() => _lines.Length > 0 ? _lines[0] : string.Empty;
public Dictionary<string, List<string>> GetTranslations()
{
var translator = new Dictionary<string, List<string>>();
if (_lines.Length <= 1)
{
return translator;
}
for (var i = 1; i < _lines.Length; i++)
{
var line = _lines[i];
var match = TranslatorRegex.Match(line);
var key = match.Groups["key"].Value;
var value = match.Groups["value"].Value;
if (translator.TryGetValue(key, out var translations))
{
translations.Add(value);
}
else
{
translator.Add(key, [value]);
}
}
return translator;
}
}
Now the test will pass. The method GetTranslations
just parses the lines loaded by ITranslatorLoader
.
Multiple Translations
Let's start by writing a test:
[Fact]
public void TestMultipleTranslations()
{
var mockTranslatorLoader = new Mock<ITranslatorLoader>();
mockTranslatorLoader
.Setup(dl => dl.GetLines())
.Returns(["en-fr", "against = contre", "against = versus"]);
var translatorParser = new TranslatorParser(mockTranslatorLoader.Object);
var expected = new Dictionary<string, List<string>>
{
{"against", ["contre", "versus"]}
};
Assert.Equal(expected, translatorParser.GetTranslations());
}
We will notice that the test will pass because we implemented TranslatorParser
. Currently, this unit does not require refactoring.
Wrong File
One of the features we haven't implemented yet is handling the loading of wrong files. This use case was not initially planned in our architecture. Let's write the test first:
[Fact]
public void TestErroneousFile()
{
var mockTranslatorLoader = new Mock<ITranslatorLoader>();
mockTranslatorLoader
.Setup(dl => dl.GetLines())
.Returns(["en-fr", "against = ", "against = "]);
var translatorParser = new TranslatorParser(mockTranslatorLoader.Object);
Assert.Throws<TranslatorException>(translatorParser.GetTranslations);
}
The test will fail. Okay, so that's what we're looking for at this step. Let's update the code to pass the test:
public partial class TranslatorParser(ITranslatorLoader loader) : ITranslatorParser
{
private static readonly Regex TranslatorRegex = new(@"^(?<key>\w+) = (?<value>\w+)$");
private readonly string[] _lines = loader.GetLines();
public string GetName() => _lines.Length > 0 ? _lines[0] : string.Empty;
public Dictionary<string, List<string>> GetTranslations()
{
var translator = new Dictionary<string, List<string>>();
if (_lines.Length <= 1)
{
return translator;
}
for (var i = 1; i < _lines.Length; i++)
{
var line = _lines[i];
var match = TranslatorRegex.Match(line);
if (!match.Success)
{
throw new TranslatorException("The file is erroneous.");
}
var key = match.Groups["key"].Value;
var value = match.Groups["value"].Value;
if (translator.TryGetValue(key, out var translations))
{
translations.Add(value);
}
else
{
translator.Add(key, [value]);
}
}
return translator;
}
}
Now, we'll notice that the test will pass because we handled the case of wrong files by throwing TranslatorException
in case of a wrong line.
Now, we will create a class that loads translator data from an external file.
Empty File
Let's start with the first test that tests an empty file:
[Fact]
public void TestEmptyFile()
{
var translatorLoader = new TranslatorLoader(@"..\..\..\..\..\data\translator-empty.txt");
Assert.Equal([], translatorLoader.GetLines());
}
The test will fail. Okay, so that's what we're looking for at this step. Now, let's write some code to pass the test using the "Fake it!" method:
public class TranslatorLoader(string path) : ITranslatorLoader
{
public string[] GetLines() => [];
}
Now the test will pass. But wait, there's code duplication. In fact, The empty string array is repeated twice in the code. So, let's do some refactoring:
public class TranslatorLoader(string path) : ITranslatorLoader
{
public string[] GetLines() => File.ReadAllLines(path);
}
Now, if we run the test again, we'll see that it passes.
Files with only Translator Names
Now, let's use the following text file (translator-name.txt):
en-fr
Let's start by writing the test:
[Fact]
public void TestTranslatorName()
{
var translatorLoader = new TranslatorLoader(@"..\..\..\..\..\data\translator-name.txt");
Assert.Equal<string[]>(["en-fr"], translatorLoader.GetLines());
}
The test will pass because we implemented the class TranslatorLoader
in the previous test. Let's move on to other units.
Files with Translations
Now, let's use the following translator file (translator.txt):
en-fr
against = contre
against = versus
Let's start by writing the test:
[Fact]
public void TestMultipleTranslations()
{
var translatorLoader = new TranslatorLoader(@"..\..\..\..\..\data\translator.txt");
Assert.Equal<string[]>(["en-fr", "against = contre", "against = versus"], translatorLoader.GetLines());
}
Again, the test will pass because we implemented the TranslatorLoader
class in previous tests.
Wrong File
Now, let's use the following translator file (translator-erroneous.txt):
en-fr
against =
against =
Let's write the test first:
[Fact]
public void TestErroneousFile()
{
var translatorLoader = new TranslatorLoader(@"..\..\..\..\..\data\translator-erroneous.txt");
Assert.Equal<string[]>(["en-fr", "against = ", "against = "], translatorLoader.GetLines());
}
Again, the test will pass because we implemented the TranslatorLoader
class in previous tests. We completed the testing and created the TranslatorLoader
class responsible for loading translator data from an external file.
We have finished coding our multilingual translator through TDD.
The following is the class diagram:
If we run all the tests, we'll notice that they all pass:
You can find test results on GitHub Actions.
Here is the code coverage:
We'll notice that we've reached 100% code coverage. This is one of the advantages of TDD.
You can find code coverage report on Codecov.
To run the source code, do the following:
- Download the source code.
- Open tdd.sln in Visual Studio 2022.
- Run all the tests in the solution.
- To get code coverage, you can use dotCover.
At first, it does kind of look like a waste of time. In the beginning of the project, it slows the development progress because we have to invest time on the hassle of writing tests, but as the project grows, it actually saves more time than the time used in the beginning. It saves time by making future changes quicker and less risky by ensuring that the changes don't break the code. It also saves time by making sure that the code that you write doesn't deviate from the real purpose and the requirements.
If the purpose of the project is clear, if the project is going to evolve over time and using TDD will save time while it grows, and if you really know what you're doing, you can then decide whether to use TDD or not.
This article demonstrates TDD in C# through a very simple example.
We can notice with TDD:
- Unit tests were written.
- We achieved 100% code coverage.
- We spent less time debugging.
- The code respects SOLID principles.
- The code is maintainable, flexible and extensible.
- The code is more coherent.
- Behavior is clear.
TDD is very useful when the code is continuously improved.
TDD provides additional benefits because developers think about software in terms of small units that can be written and tested independently and integrated together later.
- 12th December, 2023
- 3rd March, 2024
- Updated TranslatorParser.cs
- Updated Test CI workflow
- Added coverlet.msbuild
- Removed coverlet.collector
- Bump Microsoft.NET.Test.Sdk from 17.8.0 to 17.9.0
- Bump xunit from 2.6.3 to 2.7.0
- Bump xunit.runner.visualstudio from 2.5.5 to 2.5.7
- 28th August, 2024
- Bump Microsoft.NET.Test.Sdk from 17.9.0 to 17.11.0
- Bump xunit from 2.7.0 to 2.9.0
- Bump xunit.runner.visualstudio from 2.5.7 to 2.8.2