0

我理解 TDD 的原则是首先编写一个测试,观察它失败(或假设它会失败,因为还没有代码),编写一个方法,然后观察测试通过,因为它将使用你的新方法。

如果您的方法返回布尔值或整数或其他可与以下任何一种一起使用的数据类型,我会明白这是如何工作的:

Assert.AreEqual();
Assert.IsNotNull();
Assert.IsTrue();
Assert.IsFalse();
etc.

但是,我构建的功能通常是一种返回某种类型的 List 的方法,如下所示:

public static List<Employee> GetEmployees()
{
  // return list of employees
}

我如何事先为此编写 TDD 测试(或者我是否以错误的方式思考这个问题?)我显然是 TDD 的新手(到目前为止,我还没有真正看到它的任何内在价值)。

4

4 回答 4

3

要使 TDD 按预期工作,您需要从被测单元的最简单方面开始。

你的第一个测试应该是:

[TestMethod]
public void GetEmployeesReturnsAList()
{
    List<Employee> result = MyClass.GetEmployees();
    Assert.IsNotNull(result);
}

并像这样实现:

public static List<Employee> GetEmployees()
{
  return new List<Employee>();
}

现在,此测试应该在您添加代码以返回列表之前失败,并且从那时起,通过所有重构和将代码添加到被测单元,再也不会失败。

下一个测试将类似于:

// 这个测试是错误的!!它缺少可用员工的设置

我的目的是强调返回 1 名员工比返回预期的特定员工更简单。确保从更简单的情况开始。

// [TestMethod] // public void GetEmployeesReturnsOneEmployeeWhenThereIsOneEmployeeAvailable() //
{ // 列表结果 = MyClass.GetEmployees(); //
Assert.AreEqual(1, result.Count); // }

//

运行,测试失败。然后像这样重构:

public static List<Employee> GetEmployees()
{
    Employee emp = new Employee();
    List<Employee> empList = new List<Employee>();
    empList.Add(emp);

    return empList;
}

现在测试通过了。

下一个测试可能是:

[TestMethod]
public void GetEmployeesReturns_The_OneEmployeeWhenThereIsOneEmployeeAvailable()
{
    // Arrange

    Employee emp = new Employee();

    // Add code here to insert emp into the source of the list
    // This may be a mock.

    // Action
    List<Employee> result = MyClass.GetEmployees();
    Assert.AreEqual(1, result.Count);
    Assert.AreSame(emp, result[0]);
}

现在您添加并重新编写代码以使其通过而不破坏任何先前的单元测试。

冲洗并重复,添加尽可能少的代码以使测试通过而不会破坏任何其他测试。您将不得不多次重写代码,但结果是您获得了满足所有测试的最简单的代码。如果您的所有测试都完全代表了需求,那么您现在就拥有了一个功能齐全的单元。

TDD 的关键在于它可以防止不必要的复杂性,这种复杂性通常来自于使你的代码比它需要的更抽象,或者在你发现需要复杂性之前。当需要时,您总是可以事后使代码更加抽象。请注意,合法的需求甚至可能是满足代码分析工具的需求。

但是你可以自由地重构而不用担心,因为你有很好的测试覆盖率。

TDD 的另一件事是,您可能希望通过技术边缘案例测试(传递空参数并测试正确的异常)来扩充这些测试。但是,这不是必需的,这是 TDD 的另一个好处。

它强调了许多开发人员认为是“最佳实践”的事情,例如在不需要检查方法内部的方法参数时检查方法参数是否为 null,因为调用代码永远不应传入 null。如果调用代码确实传递了一个空值,那么问题就出在它而不是方法内部的代码上。

如果存在允许空值的真实代码路径,并且您的方法不期望空值,则调用代码必须自己执行空值检查并执行除调用您的方法之外的其他操作。

于 2013-09-12T16:37:31.160 回答
2

要断言集合,您可以使用CollectionAssert

通常,为了 TDD 一个函数,我想

  1. 一个简单的快乐路径场景,比如只返回 1 个元素。
  2. 一个简单的异常场景,比如什么都不返回,或者抛出异常。
  3. 一条快乐的道路,它将返回超过 1 个元素。
  4. 更复杂的路径,从所有元素中返回过滤结果。

顺便说一句,如果你想让你的函数可单元测试,请避免静态或任何全局状态或与 I/O、数据库等的通信(你可能想模拟这些真实世界的对象)

此外,如果所有外部函数状态都通过参数而不是类字段(无论是否静态)传入,那么编写测试会更容易。

于 2013-09-12T16:00:37.760 回答
1

使用流利的断言。有很多集合断言HowTo – NUnit – Fluent Assertions。您可以使用Moq进行对象模拟。

    private Mock<IActivityRepository> _mockActivityRepository;
    private Activity _expectedActivity = new Activity { Name = Name, Description = Details };

    /// <summary>
    /// Created by: kayz1
    /// Created: 23 jun 2011 23:48
    /// </summary>
    [Test, Description("GetAllActivities")]
    public void GetAllActivities_ValidProjects_ReturnProjects()
    {
        // Arrange
        var activities = new List<Activity> { _expectedActivity };
        _mockActivityRepository.Setup(x => x.GetAllActivities()).Returns(activities);
        // Act
        var resultList = _acitivityViewModel.GetAllActivities();
        // Assert
        _mockActivityRepository.VerifyAll();
        resultList.Should().HaveCount(1);
    }
于 2013-09-12T15:59:21.747 回答
1

我们想测试这种方法的开发(我正在删除静态):

public List<Employee> GetEmployees()
{
  // return list of employees
}

那么,我们能做的最低限度是多少?

public void ShouldReturnEmptyList() {
    List<Employee> list = new MyClass().GetEmployees();
    assertTrue(list.isEmpty());
}

好吧,这很容易通过,对吧?

现在让我们尝试填充列表:

public void ShouldReturnListWithFred() {
    MyClass c = new MyClass();
    Employee fred = new Employee("fred");
    c.addEmployee(fred);
    List<Employee> list = c.GetEmployees();
    assertTrue(list.contains(fred));
}

你已经完成了。当然,您也可以探索例外情况,对排序等进行断言,但基本上这就是您测试驱动开发列表返回方法的方式。

于 2013-09-12T16:24:39.210 回答