【问题标题】:Unit Testing return value of method called from inside another method从另一个方法内部调用的方法的单元测试返回值
【发布时间】:2012-01-26 11:05:07
【问题描述】:

我有一个类似的方法:

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 标志有点尴尬 IMO。
  • 可能有很多问题,我想向用户展示问题所在。你会建议什么替代方案?
  • 如果你遇到错误——意味着你得到的参数是错误的。你应该投一个exception 可能ArgumentException
  • 如果参数没有错而其他地方出错了怎么办?最好把它放在一个字符串中而不是抛出异常
  • 1.你可以抛出任何你想要的异常。 2.Exception类有一个message属性,把错误信息放在那里。

标签: c# .net unit-testing tdd


【解决方案1】:

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

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;
}

在单元测试中你应该mockIDataProvider

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

常用模拟库:

列表取自here

维基百科文章:

Dependency Injection
Inversion Of Control

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

    //...
}

【讨论】:

  • 您能否扩展您的答案并展示单元测试的样子,因为我看不到添加提供者的作用
  • 我为此添加了一个伪代码。我确定它不会编译...这只是一个插图。
  • 对我来说这似乎有点矫枉过正,因为您可以在不嘲笑的情况下解决它
  • @Mharlin。我的回答(模拟)适用于每个测试用例。而且它不会强迫您更改方法操作或逻辑(我猜您不想对每个方法使用ErrorMessgae 字符串或其他标志......)。 Mocking 为您提供最灵活的 UnitTest,但确实有时它很烦人......
  • 您向该方法添加另一个新依赖项。这增加了我认为在不需要添加它时应该避免的复杂性。而且,如果您将建议的测试方法与没有模拟的测试方法进行比较,则必须键入更多代码,并且即使最终结果相同,您也会进行一个测试,如果您更改实现,可能会中断。
【解决方案2】:

单元测试不是您添加到现有方法中间的东西,它是关于测试与系统其余部分隔离的小代码单元,以便您确信该单元的行为应如此。

因此,您应该编写第二个类,该类的唯一职责是测试DoSomething 所在的类(我们称这个类为Daddy 和测试类DaddyTests)的行为是否符合您的预期。然后,您可以编写一个调用DoSomething 的测试方法,并确保正确设置ErrorMessageErrorMessage 也应该是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 并返回
  • 您只需查看Data.Count。 (顺便说一句,一般约定是对变量、参数和字段名称使用小写首字母(驼峰式),对类、事件、属性和枚举成员名称使用大写首字母(Pascal 大小写)。)。我已经更新了答案中的示例代码。
  • 不,单元测试是关于在给定一组特定输入时测试结果是否符合预期。效率和代码清洁度与此正交但很重要,只是通常不可能在您的单元测试中涵盖。但是,某些测试框架确实允许您对调用哪些方法设置期望,因此您可以努力确保不调用 FormatData(因为您不希望在没有数据时调用它)。跨度>
  • 我开始考虑您的测试,我不妨摆脱检查空白参数等的其他测试,因为如果它们是空白的,我希望错误消息和空白列表是无论如何都返回了
  • 此外,您可以检查错误消息的内容以确保它是预期的,例如'必须指定电子邮件地址' &c.不过,对于此类测试,我倾向于酌情抛出ArgumentNullExceptionArgumentExceptionArgumentOutOfRangeException,并让我的单元测试预期此异常。
【解决方案3】:

国际海事组织,这条线

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;此外,如果您需要保留现有签名,您可以简单地将其委托给具有新的、更可测试的签名的方法。
【解决方案4】:
[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 och 为空,在这种情况下只需执行此操作

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

【讨论】:

  • 我明白你在说什么,但尽早退出而不需要调用 FormatData 不是更好吗?
  • 您可以检查传入参数是否为空或为空,在这种情况下,您会立即返回一个空列表并设置错误消息。该函数的结果仍然是相同的。所以你在测试中断言的内容不需要改变
  • 在我的回答中添加了更多细节
  • 您的意思是对 GetData 和 GetFormattedData 添加检查吗?
  • @gdoron - 我同意errorMessage 标志很尴尬,但如何处理异常取决于 API 设计人员。单元测试应该只测试所有这些,最终可以用作文档如何使用 API (请注意,我确实假设 GetData 方法是实际对象本身的方法。如果这是一个 全局方法,它确实应该被模拟/存根/DI/...)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-12-27
  • 1970-01-01
  • 1970-01-01
  • 2020-05-28
  • 1970-01-01
相关资源
最近更新 更多