C#中的高级测试驱动开发

目录

介绍

起源

目的

开发

调试

遗留代码

示例

假装!

三角测量

多个翻译

反向翻译

文件加载

DictionaryDataSourceTest

DictionaryParserTest

DictionaryLoaderTest

输入依赖关系图

检测结果

覆盖

如何运行源代码?

其他特点

结论


介绍

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

 

测试驱动开发(TDD)包括在编写代码之前编写测试,如上面的工作流程所示。

首先,测试是编写的,必须在开始时失败。然后,编写代码以便测试通过。然后,必须执行测试并且必须成功。然后,代码被重构。然后,必须再次执行测试以确保代码正确。

总结一下,这是通过五个步骤完成的:

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

我们可以注意到,在上面说明的工作流程中,测试是在重构代码之后执行的。这确保了重构后代码仍然正确。

本文将通过一个简单的例子(我们将创建一个双语词典)在C#中显示高级TDD。无论您是新手还是经验丰富的开发人员,本文都将通过一个非常简单的示例向您展示TDD的每一步。

起源

TDD基于极限编程的原理之一,也称为XP。这是一种适用于减少团队的计算机项目管理方法。

目的

TDD是一种开发方法,其中测试的编写是自动的。这是一种使用一套回归测试来交付软件的非常有效的技术。

TDD在开发快速且频繁生成软件组件的敏捷软件方法中发挥着重要作用。

开发

开发以迭代方式完成。

首先,编写测试。第一次测试必须失败。然后,编写代码以便测试通过。然后,再次执行测试并且必须通过。然后,代码被重构。然后,执行测试以确保代码的重构是正确的。这是针对一个单元完成的。然后,对于每个单元,以迭代方式执行相同的过程。

调试

 

调试对于修复单元或错误很有用。TDD用于纠正错误如下:

  • 分析问题。
  • 修复错误。
  • 运行测试以验证错误已得到修复。

如果仍然出现错误,则必须继续更正,直到所有测试都有效。

遗留代码

 

它经常发生使用或继续开发现有代码。可以通过以下方式了解现有代码:

  • 写下你想要理解的单元的测试。
  • 执行测试并确保它失败。
  • 调整测试代码直到它通过。

示例

此示例的目的是通过一个简单的例子来描述TDD的每个步骤。

该示例将在C#中开发,使用的测试框架是MSTest

我们将使用Moq进行模拟,使用JetBrains dotCover进行代码覆盖。

我们将通过TDD创建一个多语言词典。

在编写代码时,我们会尝试尊重SOLID原则。

我们还将尝试达到100%的代码覆盖率。

假装!

使用TDD时要实现的第一个任务很重要:它必须如此简单,以便快速完成循环red-green-refactor

我们首先创建一个名为的DictionaryTest测试类:

[TestClass]
public class DictionaryTest
{
}

然后,我们将在这个类中创建第一个单元测试,我们初始化一个具有"en-fr"名称的Dictionary类型的对象,我们将检查名称是否正确:

[TestClass]
public class DictionaryTest
{
    [TestMethod]
    public void TestDictionaryName()
    {
      var dict = new Dictionary("en-fr");
      Assert.AreEqual(dict.Name, "en-fr");
    }
}

测试将失败,这就是我们想要的。

现在我们得到了红色条,我们将编写代码以便测试通过。要做到这一点,有很多方法。我们将使用假装方法。具体而言,它包括通过测试所需的最低限度。在我们的例子中,编写一个返回"en-fr"的属性NameDictionary类就足够了:

public class Dictionary
{
    public string Name {get{return "en-fr";}}          
    
    public Dictionary(string name)
    {
    }
}

我们将通过在每个步骤中使用简单快速的方法逐步创建代码。

现在,如果我们再次运行我们的单元测试,它将通过。

但是等等,代码没有重构。有一个重复。确实,"en-fr"重复了两次。

我们将重构代码:

public class Dictionary
{
    public string Name { get; }          
    
    public Dictionary(string name)
    {
        Name = name;
    }
}

在重构代码之后,我们必须再次运行测试以确保代码正确。

代码重构是一种修改代码的形式,它保留了现有测试的执行,并且可以获得具有最少缺陷的软件体系结构。一些例子:

  • 删除重复的代码/移动代码。
  • 调整私有/公共属性/方法。

我们注意到我们已经完成了TDD工作流程的循环。现在,我们可以通过新测试重新开始新的循环。

已完成的单元测试对已编写的代码提供了一些信心,并允许考虑未来的变化。

三角测量

TDD中,我们首先编写测试,在编码此需求之前生成功能需求。为了完善测试,我们将应用三角测量方法。

让我们编写一个测试,检查翻译是否已添加到字典中(AddTranslation)。

验证将通过GetTranslation方法完成。

[TestMethod]
public void TestOneTranslation()
{
  var dict = new Dictionary("en-fr");
  dict.AddTranslation("against", "contre");
  Assert.AreEqual(dict.GetTranslation("against"), "contre");
}

如果我们运行测试,我们会注意到它会失败。好的,这就是我们在这一步寻找的东西。

首先,我们将使用假装方法来通过测试:

public class Dictionary
{
    public string Name { get; }          
    
    public Dictionary(string name)
    {
        Name = name;
    }
    
    public void AddTranslation(string word1, string word2)
    {
    }
    
    public string GetTranslation(string word)
    {
        return "contre";
    }
}

运行测试TestOneTranslation后,我们会注意到它会通过。

但等等,有代码重复。关键字"contre"在代码中重复两次。

我们将更改代码以删除此重复:

public class Dictionary
{
    private Dictionary<string, string> _translations;
    public string Name { get; }          
    
    public Dictionary(string name)
    {
        _translations = new Dictionary<string, string>();
        Name = name;
    }

    public void AddTranslation(string word1, string word2){
        _translations.Add(word1, word2);
    }

    public string GetTranslation(string word){
        return _translations[word];
    }
}

在重构代码之后,我们必须再次运行测试以确保代码正确。

让我们添加一个测试来检查字典是否为空: 

[TestMethod]
public void TestIsEmpty1()
{
    var dict = new Dictionary.Dictionary("en-fr");
    Assert.IsTrue(dict.IsEmpty());
}

如果我们运行测试,我们会注意到它们会失败。好的,这就是我们在这一步寻找的东西。

让我们使用假装方法并编写一些代码来通过测试:

public class Dictionary
{
    [...]

    public bool IsEmpty()
    {
        return true;
    }
}

如果我们进行测试,我们会注意到它会通过。但是等等,代码中有重复。确实,让我们解决这个问题:

public class Dictionary
{
    [...]

    public bool IsEmpty()
    {
        return _translations.Count == 0;
    }
}

如果我们再次运行测试,我们会注意到它会通过。

我们可以添加另一个单元测试来检查IsEmpty是否正确:

[TestMethod]
public void TestIsEmpty2()
{
    var dict = new Dictionary.Dictionary("en-fr");
    dict.AddTranslation("against", "contre");
    Assert.IsFalse(dict.IsEmpty());
}

如果我们进行测试,我们会注意到它会通过。

多个翻译

字典的一个特点是能够操纵多个翻译。这个用例最初并未在我们的架构中进行规划。

我们先写下测试:

[TestMethod]
public void TestMultipleTranslations()
{
  var dict = new Dictionary("en-fr");
  dict.AddTranslation("against", "contre");
  dict.AddTranslation("against", "versus");
  CollectionAssert.AreEqual(dict.GetMultipleTranslations("against"), 
                  string[]{"contre", "versus"});
}

如果我们运行测试,我们会注意到它会失败。好的,这就是我们在这一步寻找的东西。

首先,我们将使用假装方法通过修改方法AddTranslation和添加GetMultipleTranslations方法来传递测试:

public class Dictionary
{
    private readonly Dictionary<string, Dictionary<string, string>> _translations;
    public string Name { get; }          
    
    public Dictionary(string name)
    {
        _translations = new Dictionary<string, Dictionary<string, string>>();
        Name = name;
    }

    public void AddTranslation(string word1, string word2)
    {
        _translations.Add(word1, new Dictionary<string, string> {{word2, word1}});
    }

    public string[] GetMultipleTranslations(string word)
    {
      return new string[]{"contre", "versus"};
    }
    
    [...]
}

运行测试TestMultipleTranslations后,我们会注意到它会通过。

但等等,有代码重复。字符串数组new string[]{"contre", "versus"}在代码中重复两次。

我们将更改代码以删除此重复:

public class Dictionary
{
    private readonly Dictionary<string, Dictionary<string, string>> _translations;
    public string Name { get; }          
    
    public Dictionary(string name)
    {
        _translations = new Dictionary<string, Dictionary<string, string>>();
        Name = name;
    }

    public void AddTranslation(string word1, string word2)
    {
        _translations.Add(word1, new Dictionary<string, string> {{word2, word1}});
    }

    public string[] GetMultipleTranslations(string word)
    {
        return _translations[word].Keys.ToArray();
    }

    public string GetTranslation(string word)
    {
        return _translations[word][0];
    }

    public bool IsEmpty(){
        return _translations.Count == 0;
    }
}

如果我们再次运行测试,我们会注意到它会通过。

我们来做一些重构。让我们通过GetTranslation重命名GetMultipleTranslations

public class Dictionary
{
    private readonly Dictionary<string, Dictionary<string, string>> _translations;
    public string Name { get; }          
    
    public Dictionary(string name)
    {
        _translations = new Dictionary<string, Dictionary<string, string>>();
        Name = name;
    }

    public void AddTranslation(string word1, string word2)
    {
        if (!_translations.ContainsKey(word1))
        {
            _translations.Add(word1, new Dictionary<string, string> {{word2, word1}});
        }
        else
        {
            _translations[word1].Add(word2, word1);
        }
    }    

    public string[] GetTranslation(string word)
    {
        return _translations[word].Keys.ToArray();
    }

    public bool IsEmpty(){
        return _translations.Count == 0;
    }
}

我们还必须改变我们的测试:

[TestClass]
public class DictionaryTest{
    [TestMethod]
    public void TestDictionaryName()
    {
      var dict = new Dictionary("en-fr");
      Assert.AreEqual(dict.Name, "en-fr");
    }

    [TestMethod]
    public void TestOneTranslation()
    {
        var dict = new Dictionary("en-fr");
        dict.AddTranslation("against", "contre");
        Assert.AreEqual(dict.GetTranslation("against")[0], "contre");
    }

    [TestMethod]
    public void TestIsEmpty1()
    {
        var dict = new Dictionary("en-fr");
        Assert.IsTrue(dict.IsEmpty());
    }

    [TestMethod]
    public void TestIsEmpty2()
    {
        var dict = new Dictionary.Dictionary("en-fr");
        dict.AddTranslation("against", "contre");
        Assert.IsFalse(dict.IsEmpty());
    }

    [TestMethod]
    public void TestMultipleTranslations(){
        var dict = new Dictionary("en-fr");
        dict.AddTranslation("against", "contre");
        dict.AddTranslation("against", "versus");
        CollectionAssert.AreEqual(dict.GetTranslation("against"), 
                        new string[]{"contre", "versus"});
    }
}

反向翻译

现在假设我们想要考虑两个方向的翻译,例如双语词典。

让我们首先创建测试:

[TestMethod]
public void TestReverseTranslation()
{
  var dict = new Dictionary("en-fr");
  dict.AddTranslation("against", "contre");
  Assert.AreEqual(dict.GetTranslation("contre")[0], "against");
}

如果我们运行测试,我们会注意到它会失败。好的,这就是我们在这一步寻找的东西。

现在,让我们编写代码,以便通过使用假装方法传递测试:

public string[] GetTranslation(string word)
{
  if(_translations.ContainsKey(word))
  {
    return _translations[word];
  }
  else // try reverse translation
  { 
    return "against";
  }
}

测试将通过。但是有一个代码重复。确实,"against"重复了两次。所以让我们重构一下代码:

public string[] GetTranslation(string word)
{
    if (_translations.ContainsKey(word)) return _translations[word].Keys.ToArray();

    return (from t in _translations
            from v in t.Value.Values
            where t.Value.ContainsKey(word)
            select v).Distinct().ToArray();
}

行测试,我们会注意到它会通过。

文件加载

现在,我们来处理从数据源(例如外部文本文件)加载翻译。

让我们暂时专注于外部文本文件。输入格式将是一个文本文件,其中第一行包含字典的名称,其他行包含由" = "分隔的单词。

以下是一个例子:

en-fr
against = contre
against = versus

以下是我们将要执行的测试列表:

  1. 空的文件。
  2. 仅包含字典名称的文件。
  3. 带翻译的文件。
  4. 错误的文件。

首先,我们将使用模拟来编写测试。然后,我们将在此过程中编写代码。然后,我们将重构代码。最后,我们将测试代码以确保重构正确并且一切正常。

我们将创建三个新的测试类:

  • DictionaryDataSourceTest:我们将测试从外部数据源加载的字典。
  • DictionaryParserTest:我们将测试加载的字典数据的解析。
  • DictionaryLoaderTest:我们将测试从外部数据源加载的字典数据的加载。

DictionaryDataSourceTest

我们将使用Moq编写测试。

空文件名

首先,让我们编写测试:

[TestMethod]
public void TestEmptyFileName()
{
    var mockDictionaryParser = new Mock<IDictionaryParser>();
    mockDictionaryParser
        .Setup(dp => dp.GetName())
        .Returns(string.Empty);

    var dict = new Dictionary.Dictionary(mockDictionaryParser.Object);
    Assert.AreEqual(dict.Name, string.Empty);
}

测试将失败。好的,这就是我们在这一步寻找的东西。

我们将使用接口(IDictionaryParser)来解析从外部数据源加载的字典数据。

以下是接口IDictionaryParser

public interface IDictionaryParser
{
    string GetName();
}

让我们使用假装方法修改Dictionary类以通过测试:

public class Dictionary
{
    private readonly Dictionary<string, Dictionary<string, string>> _translations;

    public Dictionary(string name)
    {
        Name = name;
        _translations = new Dictionary<string, Dictionary<string, string>>();
    }

    public Dictionary(IDictionaryParser dictionaryParser)
    {
        Name = string.Empty;
    }
    
    [...]
}

如果我们再次运行测试,我们会注意到它会通过。但是等等,代码中有一个重复的东西。确实,string.Empty重复了两次。那么,让我们做一些重构:

public class Dictionary
{
    private readonly Dictionary<string, Dictionary<string, string>> _translations;

    public Dictionary(string name)
    {
        Name = name;
        _translations = new Dictionary<string, Dictionary<string, string>>();
    }

    public Dictionary(IDictionaryParser dictionaryParser)
    {
        Name = dictionaryParser.GetName();
    }
    
    [...]
}

如果我们再次运行测试,我们会注意到它会通过。

没有翻译

首先,让我们从编写测试开始:

[TestMethod]
public void TestEmptyFileTranslations()
{
    var mockDictionaryParser = new Mock<IDictionaryParser>();
    mockDictionaryParser
        .Setup(dp => dp.GetTranslations())
        .Returns(new Dictionary<string, Dictionary<string, string>>());

    var dict = new Dictionary.Dictionary(mockDictionaryParser.Object);
    CollectionAssert.AreEqual(dict.GetTranslation("against"), new string[] { });
}

我们会注意到测试会失败。好的,这就是我们在这一步寻找的东西。

首先,让我们修改接口IDictionaryParser

public interface IDictionaryParser
{
    string GetName();
    Dictionary<string, Dictionary<string, string>> GetTranslations();
}

然后,让我们使用假装方法编写一些代码来传递测试:

public class Dictionary
{
    private readonly Dictionary<string, Dictionary<string, string>> _translations;

    public Dictionary(string name)
    {
        Name = name;
        _translations = new Dictionary<string, Dictionary<string, string>>();
    }

    public Dictionary(IDictionaryParser dictionaryParser)
    {
        Name = dictionaryParser.GetName();
        _translations = new Dictionary<string, Dictionary<string, string>>();
    }
    
    [...]
}

如果我们再次运行测试,我们会注意到它会通过。但是等等,代码中有一个重复的东西。实际上,字典初始化重复两次。那么,让我们做一些重构:

public class Dictionary
{
    private readonly Dictionary<string, Dictionary<string, string>> _translations;

    public Dictionary(string name)
    {
        Name = name;
        _translations = new Dictionary<string, Dictionary<string, string>>();
    }

    public Dictionary(IDictionaryParser dictionaryParser)
    {
        Name = dictionaryParser.GetName();
        _translations = dictionaryParser.GetTranslations();
    }
    
    [...]
}

如果我们再次运行测试,我们会注意到它会通过。

只有字典名称的文件

首先,让我们从编写测试开始:

[TestMethod]
public void TestDictionaryName()
{
    var mockDictionaryParser = new Mock<IDictionaryParser>();
    mockDictionaryParser
        .Setup(dp => dp.GetName())
        .Returns("en-fr");

    var dict = new Dictionary.Dictionary(mockDictionaryParser.Object);
    Assert.AreEqual(dict.Name, "en-fr");
}

我们会注意到测试将会通过,因为我们已经编写了接口IDictionaryParser并更改了Dictionary类。目前,该单元不需要重构。

多个翻译

首先,让我们从编写测试开始:

[TestMethod]
public void TestMultipleTranslations()
{
    var mockDictionaryParser = new Mock<IDictionaryParser>();
    mockDictionaryParser
        .Setup(dp => dp.GetTranslations())
        .Returns(new Dictionary<string, Dictionary<string, string>>
        {
            { "against", new Dictionary<string, string>{{ "contre", "against" }, 
                                                        { "versus", "against" } } }
        });

    var dict = new Dictionary.Dictionary(mockDictionaryParser.Object);
    CollectionAssert.AreEqual(dict.GetTranslation("against"), new[] { "contre", "versus" });
}

我们会注意到测试将会通过,因为我们已经编写了接口IDictionaryParser并更改了Dictionary类。目前,该单元不需要重构。

错误的文件

首先,让我们从编写测试开始:

[TestMethod]
public void TestErroneousFile()
{
    var mockDictionaryParser = new Mock<IDictionaryParser>();
    mockDictionaryParser
        .Setup(dp => dp.GetTranslations())
        .Throws(new DictionaryException("The file is erroneous."));

    Assert.ThrowsException<DictionaryException>(() => new Dictionary.Dictionary(mockDictionaryParser.Object));
}

我们会注意到测试将会通过,因为我们已经编写了接口IDictionaryParser并更改了Dictionary类。目前,该单元不需要重构。

DictionaryParserTest

现在让我们通过IDictionaryLoader创建一个类来解析加载的字典数据,该类加载来自外部数据源的字典数据。

空文件名

首先,让我们从编写测试开始:

[TestMethod]
public void TestEmptyFileName()
{
    var mockDictionaryLoader = new Mock<IDictionaryLoader>();
    mockDictionaryLoader
        .Setup(dl => dl.GetLines())
        .Returns(new string[] { });

    var dictionaryParser = new DictionaryParser(mockDictionaryLoader.Object);

    Assert.AreEqual(dictionaryParser.GetName(), string.Empty);
}

测试将失败。好的,这就是我们在这一步寻找的东西。

我们将使用一个接口从外部数据源(IDictionaryLoader)加载字典数据。

以下是接口IDictionaryLoader

public interface IDictionaryLoader
{
    string[] GetLines();
}

让我们使用假装方法编写一些代码来传递测试:

public class DictionaryParser : IDictionaryParser
{
    public DictionaryParser(IDictionaryLoader dictionaryLoader)
    {
    }

    public string GetName()
    {
        return string.Empty;
    }

    public Dictionary<string, Dictionary<string, string>> GetTranslations()
    {
        return new Dictionary<string, Dictionary<string, string>>();
    }
}

测试将通过。让我们转移到其他单元。

没有翻译

让我们首先编写测试:

[TestMethod]
public void TestEmptyFileTranslations()
{
    var mockDictionaryLoader = new Mock<IDictionaryLoader>();
    mockDictionaryLoader
        .Setup(dl => dl.GetLines())
        .Returns(new string[] { });

    var dictionaryParser = new DictionaryParser(mockDictionaryLoader.Object);

    CollectionAssert.AreEqual(dictionaryParser.GetTranslations(), new Dictionary<string, Dictionary<string, string>>());
}

测试将通过。让我们转移到其他单元。

字典名称

让我们首先编写测试:

[TestMethod]
public void TestDictionaryName()
{
    var mockDictionaryLoader = new Mock<IDictionaryLoader>();
    mockDictionaryLoader
        .Setup(dl => dl.GetLines())
        .Returns(new string[] { "en-fr" });

    var dictionaryParser = new DictionaryParser(mockDictionaryLoader.Object);

    Assert.AreEqual(dictionaryParser.GetName(), "en-fr");
}

测试将失败。好的,这就是我们在这一步寻找的东西。

让我们使用假装方法编写一些代码以通过测试:

public class DictionaryParser : IDictionaryParser
{

    public DictionaryParser(IDictionaryLoader dictionaryLoader)
    {
    }

    public string GetName()
    {
        return "en-fr";
    }

    public Dictionary<string, Dictionary<string, string>> GetTranslations()
    {
        return new Dictionary<string, Dictionary<string, string>>();
    }
}

测试将通过。但等等,代码中存在重复,测试TestEmptyFileName失败。所以让我们解决这个问题:

public class DictionaryParser : IDictionaryParser
{
    private readonly string[] _lines;

    public DictionaryParser(IDictionaryLoader dictionaryLoader)
    {
        _lines = dictionaryLoader.GetLines();
    }

    public string GetName()
    {
        if (_lines.Length > 0) return _lines[0];

        return string.Empty;
    }

    public Dictionary<string, Dictionary<string, string>> GetTranslations()
    {
        return new Dictionary<string, Dictionary<string, string>>();
    }
}

现在测试将通过。

多个翻译

让我们首先编写测试:

[TestMethod]
public void TestMultipleTranslations()
{
    var mockDictionaryLoader = new Mock<IDictionaryLoader>();
    mockDictionaryLoader
        .Setup(dl => dl.GetLines())
        .Returns(new[] { "en-fr", "against = contre", "against = versus" });

    var dictionaryParser = new DictionaryParser(mockDictionaryLoader.Object);

    var expected = new Dictionary<string, Dictionary<string, string>>
    {
        {"against", new Dictionary<string, string> {{"contre", "against"},
                                                    {"versus", "against"}}}
    };

    Assert.IsTrue(dictionaryParser.GetTranslations()
        .All(kvp1 =>
            expected.ContainsKey(kvp1.Key)
            && kvp1.Value.All(kvp2 => kvp1.Value.ContainsValue(kvp2.Value))));
}

测试将失败。好的,这就是我们在这一步寻找的东西。

让我们使用假装方法编写一些代码以通过测试:

public class DictionaryParser : IDictionaryParser
{
    private readonly string[] _lines;

    public DictionaryParser(IDictionaryLoader dictionaryLoader)
    {
        _lines = dictionaryLoader.GetLines();
    }

    public string GetName()
    {
        if (_lines.Length > 0) return _lines[0];
        return string.Empty;
    }

    public Dictionary<string, Dictionary<string, string>> GetTranslations()
    {
        return new Dictionary<string, Dictionary<string, string>>
        {
            {"against", new Dictionary<string, string> {{"contre", "against"}, 
                                                        {"versus", "against"}}}
        };
    }
}

测试将通过。但等等,代码中存在重复,测试TestEmptyFileTranslations失败。所以让我们解决这个问题:

public class DictionaryParser : IDictionaryParser
{
    private readonly string[] _lines;

    public DictionaryParser(IDictionaryLoader dictionaryLoader)
    {
        _lines = dictionaryLoader.GetLines();
    }

    public string GetName()
    {
        if (_lines.Length > 0) return _lines[0];
        return string.Empty;
    }

    public Dictionary<string, Dictionary<string, string>> GetTranslations()
    {
        var dict = new Dictionary<string, Dictionary<string, string>>();

        if (_lines.Length > 1)
        {
            for (var i = 1; i < _lines.Length; i++)
            {
                var l = _lines[i];
                var regex = new Regex(@"^(?<key>\w+) \= (?<val>\w+)$");
                var match = regex.Match(l);

                var key = match.Groups["key"].Value;
                var val = match.Groups["val"].Value;

                if (!dict.ContainsKey(key))
                {
                    dict.Add(key, new Dictionary<string, string> { { val, key } });
                }
                else
                {
                    dict[key].Add(val, key);
                }
            }
        }

        return dict;
    }
}

现在测试将通过。方法GetTranslations只是解析IDictionaryLoader加载的行。

错误的文件

我们尚未实现的功能之一是处理错误文件的加载。这个用例最初并未在我们的架构中进行规划。

让我们首先编写测试:

[TestMethod]
public void TestErroneousFile()
{
    var mockDictionaryLoader = new Mock<IDictionaryLoader>();
    mockDictionaryLoader
        .Setup(dl => dl.GetLines())
        .Returns(new[] { "en-fr", "against = ", "against = " });

    var dictionaryParser = new DictionaryParser(mockDictionaryLoader.Object);

    Assert.ThrowsException<DictionaryException>(() => dictionaryParser.GetTranslations());
}

测试将失败。好的,这就是我们在这一步寻找的东西。

让我们编辑代码来通过测试:

public class DictionaryParser : IDictionaryParser
{
    private readonly string[] _lines;

    public DictionaryParser(IDictionaryLoader dictionaryLoader)
    {
        _lines = dictionaryLoader.GetLines();
    }

    public string GetName()
    {
        if (_lines.Length > 0) return _lines[0];
        return string.Empty;
    }

    public Dictionary<string, Dictionary<string, string>> GetTranslations()
    {
        var dict = new Dictionary<string, Dictionary<string, string>>();

        if (_lines.Length > 1)
        {
            for (var i = 1; i < _lines.Length; i++)
            {
                var l = _lines[i];
                var regex = new Regex(@"^(?<key>\w+) \= (?<val>\w+)$");
                var match = regex.Match(l);

                if (!match.Success)
                {
                    throw new DictionaryException("The file is erroneous.");
                }

                var key = match.Groups["key"].Value;
                var val = match.Groups["val"].Value;

                if (!dict.ContainsKey(key))
                {
                    dict.Add(key, new Dictionary<string, string> { { val, key } });
                }
                else
                {
                    dict[key].Add(val, key);
                }
            }
        }

        return dict;
    }
}

现在,我们会注意到测试将通过。

DictionaryLoaderTest

在本节中,我们将创建一个从外部文件加载字典数据的类。

空的文件

让我们从创建测试空文件的第一个测试开始:

[TestMethod]
public void TestEmptyFile()
{
    var dictionaryLoader = new DictionaryLoader("dict-empty.txt");
    CollectionAssert.AreEqual(dictionaryLoader.GetLines(), new string[] { });
}

测试将失败。好的,这就是我们在这一步寻找的东西。

现在,让我们使用假装方法编写一些代码来传递测试:

public class DictionaryLoader : IDictionaryLoader
{
    private readonly string _dictionaryPath;

    public DictionaryLoader(string dictionaryPath)
    {
        _dictionaryPath = dictionaryPath;
    }

    public string[] GetLines()
    {
        return new string[] { };
    }
}

现在测试将通过。但是等等,有代码重复。实际上,new string[] { }在代码中重复了两次。那么,让我们做一些重构:

public class DictionaryLoader : IDictionaryLoader
{
    private readonly string _dictionaryPath;

    public DictionaryLoader(string dictionaryPath)
    {
        _dictionaryPath = dictionaryPath;
    }

    public string[] GetLines()
    {
        return File.ReadAllLines(_dictionaryPath);
    }
}

现在,如果我们再次运行测试,我们会发现它会通过。

只有字典名称的文件

现在,让我们使用以下文本文件(dict-name.txt):

en-fr

让我们从编写测试开始:

[TestMethod]
public void TestDictionaryName()
{
  var dictionaryLoader = new DictionaryLoader("dict-name.txt");
  CollectionAssert.AreEqual(dictionaryLoader.GetLines(), new[] { "en-fr" });
}

测试将通过,因为我们在上一个测试中实现了类DictionaryLoader

让我们转移到其他单元。

带翻译的文件

现在,让我们使用以下字典文件(dict.txt):

en-fr
against = contre
against = versus

首先,让我们从编写测试开始:

[TestMethod]
public void TestMultipleTranslations()
{
    var dictionaryLoader = new DictionaryLoader("dict.txt");
    CollectionAssert.AreEqual(dictionaryLoader.GetLines(), new[] { "en-fr", "against = contre", "against = versus" });
}

同样,测试将通过,因为我们在之前的测试中实现了DictionaryLoader类。

错误的文件

现在,让我们使用以下字典文件(dict-erroneous.txt):

en-fr
against = 
against =

让我们首先编写测试:

[TestMethod]
public void TestErroneousFile()
{
    var dictionaryLoader = new DictionaryLoader("dict-erroneous.txt");
    CollectionAssert.AreEqual(dictionaryLoader.GetLines(), new[] { "en-fr", "against = ", "against = " });
}

同样,测试将通过,因为我们在之前的测试中实现了DictionaryLoader类。

我们完成了测试并创建了负责从外部文件加载字典数据的DictionaryLoader类。

输入依赖关系图

下面是类型依赖关系图:

 

以下是类图:

 

我们将注意到所有SOLID原则都得到了尊重,并且代码是可维护的,灵活的和可扩展的。

检测结果

如果我们运行所有测试,我们会注意到它们都通过了:

 

我们会注意到所有的单元测试都已经编写完成。这是TDD的优势之一。

覆盖

以下是使用JetBrains dotCover的代码覆盖率:

 

我们会注意到我们达到了100%的代码覆盖率。这是TDD的优势之一。

如何运行源代码?

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

  1. 从本文顶部下载源代码。
  2. 通过以下命令恢复nuget包:nuget restore tdd.sln
  3. Visual Studio 2019中打开tdd.sln
  4. Visual Studio 2019构建解决方案。
  5. 运行解决方案中的所有单元测试。
  6. 要获得代码覆盖率,请下载并安装JetBrains dotCover,然后在Visual Studio 2019中打开解决方案tdd.sln,然后右键单击DictionaryTest项目,最后单击“Cover Unit Tests”

其他特点

另一个特点是在Dictionary类中的修改方法AddTranslation,它为了更新字典的外部文本文件。

最后,另一个特点是从数据库中读取翻译并将翻译写入数据库。

结论

本文通过一个简单的例子展示了C#中的高级TDD

我们可以通过TDD注意到:

  • 单元测试是编写的。
  • 我们达到了100%的代码覆盖率。
  • 我们花在调试上的时间更少。
  • 该代码尊重SOLID原则。
  • 代码是可维护的、灵活的和可扩展的。
  • 代码更加连贯。
  • 行为清晰。
  • 代码更强壮。
  • 没有回归。

当代码不断改进时,TDD非常有用。

TDD提供了其他好处,因为开发人员从小单元的角度考虑软件,这些小单元可以独立地编写和测试,并在以后集成在一起。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值