C# 中的测试驱动开发





5.00/5 (10投票s)
通过一个简单的例子了解 C# 中的 TDD
目录
引言
编写单元测试的传统方法是编写测试来检查代码的有效性。首先,您开始编写代码,然后编写测试。这与测试驱动开发相反。
测试驱动开发 (TDD) 涉及在编写代码之前编写测试,如上图所示。
首先,编写测试,一开始必须失败。然后,我们编写代码,使测试通过。然后,必须执行测试并且必须成功。然后,代码得到重构。然后,必须再次执行测试以确保代码正确。
总而言之,这分为五个步骤
- 编写一个测试。
- 一开始测试必须失败。
- 编写代码,使测试通过。
- 执行测试并确保它通过。
- 重构代码。
我们可以注意到,在上一步骤的流程中,代码重构后会执行测试。这确保了代码在重构后仍然是正确的。
本文将通过一个简单的例子讨论 C# 中的 TDD。该示例的目的是描述 TDD 的每个步骤。该示例将在 C# 中开发,使用的测试框架是 xUnit。我们将使用 Moq 进行模拟,使用 dotCover 进行代码覆盖率。我们将通过 TDD 创建一个多语言翻译器。在编写代码时,我们将努力遵守 SOLID 原则并实现 100% 的代码覆盖率。
开发环境
- Visual Studio 2022 >= 17.8.0
- .NET 8.0
必备组件
- C#
- xUnit
- Moq
随便写写!
使用 TDD 时要实现的第一项任务很重要:它必须足够简单,以便能够快速完成红-绿-重构循环。
我们首先创建一个名为 TranslatorTest
的测试类
public class TranslatorTest
{
}
然后,我们将在此类中创建第一个单元测试,我们初始化一个类型为 Translator
的对象,其名称为 "en-fr"
,然后检查名称是否正确
public class TranslatorTest
{
[Fact]
public void TestTranslatorName()
{
var translator = new Translator("en-fr");
Assert.Equal("en-fr", translator.Name);
}
}
测试将失败,这正是我们想要的。
现在我们已经达到了红灯阶段,我们将编写一些代码以使测试通过。有很多方法可以做到这一点。我们将使用“随便写写!
”方法。具体来说,它包括通过测试所需的最低限度。在我们的例子中,编写一个返回 Translator
类的 Name
属性为 "en-fr"
的类就足够了。
public class Translator
{
public string Name => "en-fr";
}
我们将通过在每个步骤中使用简单快捷的方法来逐步创建代码。现在,如果我们再次运行单元测试,它将通过。但代码尚未重构。存在冗余。确实,"en-fr"
重复了两次。我们将重构代码
public class Translator(string name)
{
public string Name => name;
}
重构代码后,我们必须再次运行测试以确保代码正确。
代码重构是一种修改代码的方法,它保留现有测试的执行,并获得具有最少缺陷的软件架构。一些例子
- 删除重复代码/移动代码。
- 调整
private
/public
属性/方法。
我们注意到我们已经完成了 TDD 工作流程的循环。现在我们可以一遍又一遍地开始新的测试循环。
三角剖分
在 TDD 中,我们先编写测试,在编写代码之前生成功能需求。为了完善测试,我们将应用三角剖分方法。
让我们编写一个测试来检查是否已将翻译添加到翻译器(AddTranslation
)。
测试将通过 GetTranslation
方法进行。
[Fact]
public void TestOneTranslation()
{
var translator = new Translator("en-fr");
translator.AddTranslation("against", "contre");
Assert.Equal("contre", translator.GetTranslation("against"));
}
如果我们运行测试,我们会注意到它失败了。好的,这就是我们在这一步所寻找的。
首先,我们将使用“随便写写!”方法来通过测试
public class Translator(string name)
{
public string Name => name;
public void AddTranslation(string word, string translation)
{
}
public string GetTranslation(string word) => "contre";
}
运行测试 TestOneTranslation
后,我们会注意到它通过了。
但是等等,有代码重复。关键字 "contre"
在代码中重复了两次。我们将更改代码以消除这种重复
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];
}
重构代码后,我们必须再次运行测试以确保代码正确。
让我们添加一个测试来检查翻译器是否为空
[Fact]
public void TestIsEmpty()
{
var translator = new Translator("en-fr");
Assert.True(translator.IsEmpty());
}
如果我们运行测试,我们会注意到它失败了。好的,这就是我们在这一步所寻找的。让我们使用“随便写写!”方法并编写一些代码来通过测试
public class Translator(string name)
{
[...]
public bool IsEmpty() => true;
}
如果我们运行测试,我们会注意到它通过了。
现在,让我们使用三角剖分技术,使用两个断言来驱动代码的泛化
[Fact]
public void TestIsEmpty()
{
var translator = new Translator("en-fr");
Assert.True(translator.IsEmpty());
translator.AddTranslation("against", "contre");
Assert.False(translator.IsEmpty());
}
现在如果我们再次运行测试,它将因为第二个断言而失败。这就是所谓的三角剖分。
所以让我们来修复这个问题
public class Translator(string name)
{
[...]
public bool IsEmpty() => _translations.Count == 0;
}
如果我们再次运行测试,我们会注意到它通过了。
多重翻译
翻译器的一个特点是能够处理多个翻译。这个用例最初不在我们的架构计划中。让我们先编写测试
[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"));
}
如果我们运行测试,我们会注意到它失败了。好的,这就是我们在这一步所寻找的。首先,我们将使用“随便写写!”方法来修改 AddTranslation
方法并通过添加 GetMultipleTranslations
方法来通过测试
public class Translator(string name)
{
private readonly Dictionary<string, List<string>> _translations = new();
public string Name => name;
public string[] GetMultipleTranslation(string word) => ["contre", "versus"];
[...]
}
运行测试 TestMultipleTranslations
后,我们会注意到它通过了。但是等等,有代码重复。字符串数组 ["contre", "versus"]
在代码中重复了两次。我们将更改代码以消除这种重复
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;
}
如果我们再次运行测试,我们会注意到它通过了。让我们重构一下,并将 GetMultipleTranslations
重命名为 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;
}
我们还必须更改我们的测试
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"));
}
}
反向翻译
现在假设我们想考虑双向翻译,例如,双语翻译器。让我们先创建测试
[Fact]
public void TestReverseTranslation()
{
var translator = new Translator("en-fr");
translator.AddTranslation("against", "contre");
Assert.Equal<string[]>(["against"], translator.GetTranslation("contre"));
}
如果我们运行测试,我们会注意到它失败了。好的,这就是我们在这一步所寻找的。现在,让我们编写代码以使用“随便写写!”方法通过测试
public string[] GetTranslation(string word)
{
if (_translations.TryGetValue(word, out var translations))
{
return [.. translations];
}
// Try reverse translation
return ["against"];
}
测试将通过。但是存在代码重复。确实,"against"
重复了两次。所以,让我们重构代码
public string[] GetTranslation(string word)
{
if (_translations.TryGetValue(word, out var translations))
{
return [.. translations];
}
// Try reverse translation
return [.. from t in _translations
where t.Value.Contains(word)
select t.Key];
}
如果我们再次运行测试,我们会注意到它通过了。
文件加载
现在,让我们处理从数据源(例如外部文本文件)加载翻译。暂时先关注外部文本文件。输入格式将是一个文本文件,第一行包含翻译器的名称,其他行包含用 " = "
分隔的单词。示例如下
en-fr
against = contre
against = versus
这是我们将执行的测试列表
- 空文件。
- 只包含翻译器名称的文件。
- 包含翻译的文件。
- 错误的文件。
首先,我们将使用模拟来编写测试。然后我们将边写边编写代码。然后我们将重构代码。最后,我们将测试代码以确保我们重构正确并且一切正常。我们将创建三个新的测试类
TranslatorDataSourceTest
:我们将测试从外部数据源加载的翻译器。TranslatorParserTest
:我们将测试加载的翻译器数据的解析。TranslatorLoaderTest
:我们将测试从外部数据源加载的翻译器数据的加载。
TranslatorDataSourceTest
空的翻译器名称
首先,让我们编写测试
[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);
}
测试将失败。好的,这就是我们在这一步所寻找的。我们将使用 ITranslatorParser
接口来解析从外部数据源加载的翻译器数据。以下是 ITranslatorParser
接口
public interface ITranslatorParser
{
string GetName();
}
让我们使用“随便写写!”方法修改 Translator
类以通过测试
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;
}
[...]
}
如果我们再次运行测试,我们会注意到它通过了。但是等等,代码中有重复。确实,string.Empty
重复了两次。所以,让我们进行一些重构
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();
}
}
如果我们再次运行测试,我们会注意到它通过了。
无翻译
首先,让我们从编写测试开始
[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"));
}
我们会注意到测试将失败。好的,这就是我们在这一步所寻找的。首先,让我们修改 ITranslatorParser
接口
public interface ITranslatorParser
{
string GetName();
Dictionary<string, List<string>> GetTranslations();
}
然后,让我们编写一些代码以使用“随便写写!”方法通过测试
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();
}
[...]
}
如果我们再次运行测试,我们会注意到它通过了。但是等等,代码中有重复。实际上,翻译器的初始化重复了两次。所以,让我们进行一些重构
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();
}
[...]
}
如果我们再次运行测试,我们会注意到它通过了。
仅包含翻译器名称的文件
首先,让我们从编写测试开始
[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);
}
我们会注意到测试将通过,因为我们已经编写了 ITranslatorParser
接口并更改了 Translator
类。目前,此单元不需要重构。
一次翻译
首先,让我们从编写测试开始
[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"));
}
我们会注意到测试将通过,因为我们已经编写了 ITranslatorParser
接口并更改了 Translator
类。目前,此单元不需要重构。
多重翻译
首先,让我们从编写测试开始
[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"));
}
我们会注意到测试将通过,因为我们已经编写了 ITranslatorParser
接口并更改了 Translator
类。目前,此单元不需要重构。
错误的文件
首先,让我们从编写测试开始
[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));
}
我们会注意到测试将通过,因为我们已经编写了 ITranslatorParser
接口并更改了 Translator
类。目前,此单元不需要重构。
TranslatorParserTest
现在让我们创建一个类来解析通过 ITranslatorLoader
加载的翻译器数据,它从外部数据源加载翻译器数据。
空的翻译器名称
首先,让我们从编写测试开始
[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());
}
测试将失败。好的,这就是我们在这一步所寻找的。我们将使用 ITranslatorLoader
接口从外部数据源加载翻译器数据。以下是 ITranslatorLoader
接口
public interface ITranslatorLoader
{
string[] GetLines();
}
让我们编写一些代码以使用“随便写写!”方法通过测试
public class TranslatorParser(ITranslatorLoader loader) : ITranslatorParser
{
public string GetName() => string.Empty;
public Dictionary<string, List<string>> GetTranslations() => new();
}
测试将通过。让我们继续处理其他单元。
无翻译
让我们从编写测试开始
[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());
}
测试将通过。让我们继续处理其他单元。
翻译器名称
让我们从编写测试开始
[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());
}
测试将失败。好的,这就是我们在这一步所寻找的。让我们编写一些代码以使用“随便写写!”方法通过测试
public class TranslatorParser(ITranslatorLoader loader) : ITranslatorParser
{
public string GetName() => "en-fr";
public Dictionary<string, List<string>> GetTranslations() => new();
}
测试将通过。但是等等,代码中有重复,并且测试 TestEmptyTranslatorName
失败了。所以让我们来解决这个问题
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();
}
现在,测试将通过。
一次翻译
让我们从编写测试开始
[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());
}
测试将失败。好的,这就是我们在这一步所寻找的。让我们编写一些代码以使用“随便写写!”方法通过测试
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"]}
};
}
测试将通过。但是等等,代码中有重复,并且测试 TestNoTranslation
失败了。所以让我们来修复这个问题
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;
}
}
现在测试将通过。GetTranslations
方法只是解析 ITranslatorLoader
加载的行。
多重翻译
让我们从编写测试开始
[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());
}
我们会注意到测试将通过,因为我们实现了 TranslatorParser
。目前,此单元不需要重构。
错误的文件
我们尚未实现的功能之一是处理加载错误文件。这个用例最初不在我们的架构计划中。让我们先编写测试
[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);
}
测试将失败。好的,这就是我们在这一步所寻找的。让我们更新代码以通过测试
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;
}
}
现在,我们会注意到测试将通过,因为我们通过在行错误时抛出 TranslatorException
来处理了错误文件的案例。
TranslatorLoaderTest
现在,我们将创建一个类,该类从外部文件加载翻译器数据。
空文件
让我们从测试空文件的第一个测试开始
[Fact]
public void TestEmptyFile()
{
var translatorLoader = new TranslatorLoader(@"..\..\..\..\..\data\translator-empty.txt");
Assert.Equal([], translatorLoader.GetLines());
}
测试将失败。好的,这就是我们在这一步所寻找的。现在,让我们编写一些代码以使用“随便写写!”方法通过测试
public class TranslatorLoader(string path) : ITranslatorLoader
{
public string[] GetLines() => [];
}
现在测试将通过。但是等等,存在代码重复。事实上,空字符串数组在代码中重复了两次。所以,让我们进行一些重构
public class TranslatorLoader(string path) : ITranslatorLoader
{
public string[] GetLines() => File.ReadAllLines(path);
}
现在,如果我们再次运行测试,我们会看到它通过了。
仅包含翻译器名称的文件
现在,让我们使用以下文本文件(translator-name.txt)
en-fr
让我们从编写测试开始
[Fact]
public void TestTranslatorName()
{
var translatorLoader = new TranslatorLoader(@"..\..\..\..\..\data\translator-name.txt");
Assert.Equal<string[]>(["en-fr"], translatorLoader.GetLines());
}
测试将通过,因为我们在上一个测试中实现了 TranslatorLoader
类。让我们继续处理其他单元。
包含翻译的文件
现在,让我们使用以下翻译器文件(translator.txt)
en-fr
against = contre
against = versus
让我们从编写测试开始
[Fact]
public void TestMultipleTranslations()
{
var translatorLoader = new TranslatorLoader(@"..\..\..\..\..\data\translator.txt");
Assert.Equal<string[]>(["en-fr", "against = contre", "against = versus"], translatorLoader.GetLines());
}
同样,测试将通过,因为我们在之前的测试中实现了 TranslatorLoader
类。
错误的文件
现在,让我们使用以下翻译器文件(translator-erroneous.txt)
en-fr
against =
against =
让我们先编写测试
[Fact]
public void TestErroneousFile()
{
var translatorLoader = new TranslatorLoader(@"..\..\..\..\..\data\translator-erroneous.txt");
Assert.Equal<string[]>(["en-fr", "against = ", "against = "], translatorLoader.GetLines());
}
同样,测试将通过,因为我们在之前的测试中实现了 TranslatorLoader
类。我们完成了测试并创建了负责从外部文件加载翻译器数据的 TranslatorLoader
类。
类图
我们已经完成了通过 TDD 编码多语言翻译器。
以下是类图
测试结果
如果我们运行所有测试,我们会注意到它们都通过了
您可以在 GitHub Actions 上找到测试结果。
代码覆盖率
这是代码覆盖率
我们会注意到我们达到了 100% 的代码覆盖率。这是 TDD 的优点之一。
您可以在 Codecov 上找到代码覆盖率报告。
运行源代码
要运行源代码,请执行以下操作
- 下载源代码。
- 在 Visual Studio 2022 中打开 tdd.sln。
- 运行解决方案中的所有测试。
- 要获得代码覆盖率,您可以使用 dotCover。
TDD 是浪费时间吗?
起初,它确实有点像在浪费时间。在项目初期,它会减慢开发进度,因为我们必须花时间在编写测试的麻烦上,但随着项目的增长,它实际上比初期花费的时间节省了更多时间。它通过确保更改不会破坏代码,从而使将来的更改更快、风险更低来节省时间。它还通过确保您编写的代码不偏离实际目标和需求来节省时间。
如果项目目标明确,项目将随时间发展,并且使用 TDD 将随着项目的增长节省时间,如果您确实知道自己在做什么,那么您可以决定是否使用 TDD。
结论
本文通过一个非常简单的例子演示了 C# 中的 TDD。
我们可以注意到 TDD 的作用
- 单元测试已编写。
- 我们实现了 100% 的代码覆盖率。
- 我们花了更少的时间调试。
- 代码遵守 SOLID 原则。
- 代码是可维护的、灵活的和可扩展的。
- 代码更连贯。
- 行为清晰。
TDD 在代码不断改进时非常有用。
TDD 提供了额外的优势,因为开发人员以可以独立编写和测试、稍后集成的小单元来思考软件。
历史
- 2023 年 12 月 12 日
- 首次发布
- 2024 年 3 月 3 日
- 更新了 TranslatorParser.cs
- 更新了 测试 CI 工作流
- 添加了 coverlet.msbuild
- 删除了 coverlet.collector
- 将 Microsoft.NET.Test.Sdk 从 17.8.0 升级到 17.9.0
- 将 xunit 从 2.6.3 升级到 2.7.0
- 将 xunit.runner.visualstudio 从 2.5.5 升级到 2.5.7
- 2024 年 8 月 28 日
- 将 Microsoft.NET.Test.Sdk 从 17.9.0 升级到 17.11.0
- 将 xunit 从 2.7.0 升级到 2.9.0
- 将 xunit.runner.visualstudio 从 2.5.7 升级到 2.8.2