单元测试内部方法调用的返回值

5

我有一个类似于这个的方法:

public List<MyClass> DoSomething(string Name, string Address, string Email, ref string ErrorMessage)
{
  //Check for empty string parameters etc now go and get some data   
  List<MyClass> Data = GetData(Name, Address, Email);

  /*************************************************************    
  //How do I unit test that the data variable might be empty???    
  *************************************************************/

  List<MyClass> FormattedData = FormatData(Data);
  return FormattedData;
}

我正在学习TDD/单元测试。我的问题是,如何编写测试来确保如果GetData返回一个空列表,我会将ErrorMessage设置为某些内容,然后返回一个空列表?


在我看来,使用 ErrorMessage 标志有点笨拙。 - gdoron
可能会出现许多问题,我想向用户展示问题所在。你有什么其他建议吗? - Jon
如果你遇到了一个错误,意味着你得到的参数是错误的。你应该抛出一个exception,可能是ArgumentException - gdoron
如果参数没有问题,但其他事情出了问题怎么办?最好将其放在字符串中而不是抛出异常。 - Jon
  1. 你可以抛出任何类型的异常。
  2. Exception 类有一个消息属性,在那里放置错误消息。
- gdoron
4个回答

4

在开发时,您应该留下一个像这样的“入口点”进行测试:

public List<MyClass> DoSomething(string Name, string Address,
              string Email, ref string ErrorMessage, IDataProvider provider)
{
  //Check for empty string parameters etc now go and get some data   
  List<MyClass> Data = provider.GetData(Name, Address, Email);

  List<MyClass> FormattedData = FormatData(Data);
  return FormattedData;
}

在单元测试中,你应该 mock IDataProvider

这被称为依赖注入(DI)或控制反转(IOC)

常见的模拟库:

这个列表是从 这里 获取的。

维基百科文章:

依赖注入
控制反转


NUnit 伪代码:

[Test]
public void When_user_forgot_password_should_save_user()
{
    // Arrange
    var errorMessage = string.Empty;
    var dataProvider = MockRepository.GenerateStub<IDataProvider>();
    dataProvider.Stub(x => x.GetData("x", "y", "z", ref errorMessage )).Return(null);

    // Act
    var result DoSomething("x","y","z", ref errorMessage, dataProvider);

    // Assert

    //...
}

你能否详细说明一下你的答案,并展示一下单元测试的样例?因为我无法看出添加提供程序会产生什么影响。 - Jon
我添加了一个伪代码。我确定它不会编译……这只是一个示例。 - gdoron
2
对我来说,当你可以在不使用模拟的情况下解决问题时,这似乎有点过头了。 - Mharlin
@Mharlin。我的答案(Mocking)适用于每个测试用例。它不会强制你更改方法操作或逻辑(我猜你不想在每个方法中使用“ErrorMessgae”字符串或其他标志...)。Mocking为您提供了最灵活的单元测试,但有时确实很烦人... - gdoron
1
你在方法中添加了一个新的依赖项。这增加了复杂性,我认为在不必要时应该避免添加它。如果你将你建议的测试方法与没有模拟的方法进行比较,你需要输入更多的代码,并且你还会创建一个测试,即使最终结果相同,但如果你改变实现方式,它也可能会出错。 - Mharlin

3

单元测试不是将其添加到现有方法中,而是在与系统的其余部分隔离的情况下测试小的代码单元,以使您有信心该单元的行为符合预期。

因此,您应编写第二个类,该类的唯一责任是测试包含DoSomething的类(称此类为Daddy,测试类为DaddyTests)是否按照预期运行。然后,您可以编写一个测试方法,调用DoSomething并确保适当设置了ErrorMessage(也应将ErrorMessage设置为out参数,而不是ref,除非您还传递了一个值)。

为了方便这个测试,您需要确保GetData返回没有数据。在虚拟提供程序中可以通过传入空数据集来轻松实现。但在更复杂的情况下,可能需要将整个类替换为虚拟/模拟等效类:使用接口和依赖注入使此任务非常简单。(通常,在Daddy的构建过程中设置提供程序,而不是在调用DoSomething时作为参数设置。)

public class Daddy {
    public List<MyClass> DoSomething(string Name, string Address,                  string Email, out string ErrorMessage, IDataProvider provider)
    {
      //Check for empty string parameters etc now go and get some data   
      List<MyClass> Data = provider.GetData(Name, Address, Email);

      if (Data.Count == 0)
      {
          ErrorMessage = "Oh noes";
          return Enumerable.Empty<MyClass>();
      }

      List<MyClass> formattedData = FormatData(Data);
      return formattedData;
    }
}

[TestClass]
public class DaddyTest {
    [TestMethod]
    public void DoSomethingHandlesEmptyDataSet() {
        // set-up
        Daddy daddy = new Daddy();

        // test
        IList<MyClass> result = daddy.DoSomething("blah",
                                                  "101 Dalmation Road",
                                                  "bob@example.com",
                                                  out error,
                                                  new FakeProvider(new Enumerable.Empty<AcmeData>())); // a class we've written to act in lieu of the real provider

        // validate
        Assert.NotNull(result); // most testing frameworks provides Assert functionality
        Assert.IsTrue(result.Count == 0);
        Assert.IsFalse(String.IsNullOrEmpty(error));
    }
}

}


我认为我理解了,但是我无法解决如何测试变量Data是否为空以设置ErrorMessage并返回的问题。 - Jon
你只需要查看 Data.Count。(顺便说一下,通常约定变量、参数和字段名使用小写字母开头(驼峰命名法),而类、事件、属性和枚举成员名使用大写字母开头(帕斯卡命名法)。)我已经更新了答案中的示例代码。 - Paul Ruane
不,单元测试是关于测试在给定特定输入时结果是否符合预期。效率和代码清洁度与此无关,但它们很重要,只是通常无法在单元测试中覆盖。然而,一些测试框架确实允许您设置对调用哪些方法的期望,因此,您可以通过努力确保不调用FormatData(因为当没有数据时您不希望调用它)。 - Paul Ruane
我开始认为,根据您的测试,我可能应该摆脱我的其他测试,这些测试检查空参数等,因为如果它们为空,我希望会返回一个错误消息和一个空列表。 - Jon
此外,您可以检查错误消息的内容以确保它是预期的,例如“必须指定电子邮件地址”等。对于这类测试,我倾向于根据需要抛出 ArgumentNullExceptionArgumentExceptionArgumentOutOfRangeException,并让我的单元测试期望这个异常。 - Paul Ruane

1
[TestMethod]
public void MyDoSomethingTest()
{
   string errorMessage = string.Empty;
   var actual = myClass.DoSomething(..., ref errorMessage)
   Assert.AreEqual("MyErrorMessage", errorMessage);
   Assert.AreEqual(0, FormattedData.Count);
}

我假设如果没有数据来格式化,格式化程序将返回一个空列表。

由于您想要验证方法的最终结果,因此我不会尝试找出从GetData函数返回了什么,因为您想要验证的是实际返回值是否为空列表,以及FormatData是否崩溃。

如果您想尽快退出函数,可以检查任何参数是否为null或为空,在这种情况下只需执行

errorMessage = "Empty parameters are not allowed";
return new List<MyClass>();

我明白你的意思,但是尽早退出是否更好,从而避免调用FormatData函数呢? - Jon
你可以加入一个检查,如果传入的参数是null或空的话,直接返回一个空列表并设置错误消息。函数的结果仍然是相同的。因此,在测试中所断言的内容不需要更改。 - Mharlin
我在我的回答中添加了更多细节。 - Mharlin
@Lieven。这是一个简单的答案,但不一定是最好的。使用errorMessage标志是笨拙的。 - gdoron
2
@gdoron - 我同意errorMessage标志很尴尬,但是如何处理异常取决于API设计者。单元测试只应该测试所有这些内容,并且最终可以用作文档,说明API的使用方式(请注意,我假设GetData方法是实际对象本身的方法。如果这是一个<cough>全局方法,则确实应该进行模拟/存根/DI/...)。 - Lieven Keersmaekers
显示剩余3条评论

1

在我看来,这行代码

List<MyClass> Data = GetData(Name, Address, Email);

应该在类外面。将方法签名更改为

public List<MyClass> DoSomething(List<MyClass> data, ref string ErrorMessage)

这种方法变得更容易测试,因为您可以轻松地改变输入以测试所有可能的边缘情况。

另一种选择是通过模拟依赖项来公开GetData方法,然后设置返回各种结果。因此,您的类现在看起来像:

class ThisClass
{
   [Import]
   public IDataService DataService {get; set;}

   public List<MyClass> DoSomething(string Name, string Address, string Email, ref string ErrorMessage)
   {
     //Check for empty string parameters etc now go and get some data   
     List<MyClass> Data = IDataService.GetData(Name, Address, Email); // using dependency

     List<MyClass> FormattedData = FormatData(Data);
     return FormattedData;
   }
}

如果需要保留现有的签名,您可以将其委托给具有新的、更易于测试的签名的方法。+1; - Carl Manaster

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接