65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 中的测试驱动开发

starIconstarIconstarIconstarIconstarIcon

5.00/5 (10投票s)

2023 年 12 月 12 日

CPOL

13分钟阅读

viewsIcon

45510

downloadIcon

478

通过一个简单的例子了解 C# 中的 TDD

目录

  1. 引言
  2. 开发环境
  3. 必备组件
  4. 随便写写!
  5. 三角剖分
  6. 多重翻译
  7. 反向翻译
  8. 文件加载
    1. TranslatorDataSourceTest
    2. TranslatorParserTest
    3. TranslatorLoaderTest
  9. 类图
  10. 测试结果
  11. 代码覆盖率
  12. 运行源代码
  13. TDD 是浪费时间吗?
  14. 结论
  15. 历史

引言

编写单元测试的传统方法是编写测试来检查代码的有效性。首先,您开始编写代码,然后编写测试。这与测试驱动开发相反。

测试驱动开发 (TDD) 涉及在编写代码之前编写测试,如上图所示。

首先,编写测试,一开始必须失败。然后,我们编写代码,使测试通过。然后,必须执行测试并且必须成功。然后,代码得到重构。然后,必须再次执行测试以确保代码正确。

总而言之,这分为五个步骤

  1. 编写一个测试。
  2. 一开始测试必须失败。
  3. 编写代码,使测试通过。
  4. 执行测试并确保它通过。
  5. 重构代码。

我们可以注意到,在上一步骤的流程中,代码重构后会执行测试。这确保了代码在重构后仍然是正确的。

本文将通过一个简单的例子讨论 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

这是我们将执行的测试列表

  1. 空文件。
  2. 只包含翻译器名称的文件。
  3. 包含翻译的文件。
  4. 错误的文件。

首先,我们将使用模拟来编写测试。然后我们将边写边编写代码。然后我们将重构代码。最后,我们将测试代码以确保我们重构正确并且一切正常。我们将创建三个新的测试类

  • 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 上找到代码覆盖率报告。

运行源代码

要运行源代码,请执行以下操作

  1. 下载源代码。
  2. 在 Visual Studio 2022 中打开 tdd.sln
  3. 运行解决方案中的所有测试。
  4. 要获得代码覆盖率,您可以使用 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
© . All rights reserved.