首页
学习
活动
专区
工具
TVP
发布
社区首页 >问答首页 >如何将系统设计与单元测试分离(如Bob叔叔所建议的那样)?

如何将系统设计与单元测试分离(如Bob叔叔所建议的那样)?
EN

Stack Overflow用户
提问于 2018-09-28 05:47:26
回答 2查看 0关注 0票数 0

鲍勃叔叔(Bob Martin)在他的博客中提到,为了将我们系统的设计与单元测试分离,我们不应该将我们的具体类直接暴露给单元测试。相反,我们应该公开一个代表我们系统的API,然后使用这个API进行单元测试。

粗略地表达了鲍勃叔叔的建议

根据我的理解,我认为通过API,他的意思是一个界面。所以单元测试应该与接口而不是真正的类进行交互。

我的问题是:如果我们只公开单元测试的接口,那么这些单元测试如何访问实际的实现来验证它们的行为?我们应该在测试中使用DI来在运行时注入实际类吗?有没有办法让下面的代码工作?

ILoanEligibility.cs

public interface ILoanEligibility
{
    bool HasCorrectType(string loanType);
}

LoanEligibility.cs

public class LoanEligibility : ILoanEligibility
{
    public bool HasCorrectType(string loanType)
    {
        if(loanType.Equals("Personal"))
        {
            return true;
        }
        return false;
    }
}

单元测试

[TestClass]
public class LoanEligibilityTest
{
    ILoanEligibility _loanEligibility;
    [TestMethod]
    public void TestLoanTypePersonal()
    {
        //Arrange
        string loanType = "Personal";

        //Act
        bool expected = _loanEligibility.HasCorrectType(loanType);

        //Assert
        Assert.IsTrue(expected);
    }
}

上面的单元测试试图查看LoanEligibility.HasCorrectType()方法是否适用于“个人”类型。显然,测试将失败,因为我们没有使用具体的类,而是根据Bob叔叔的建议(如果我理解正确的话)使用接口。

如何让这个测试通过?任何的意见都将会有帮助。

编辑1 感谢@bleepzter建议Moq。以下是修改后的单元测试类,测试有效和无效情况。

[TestClass]
public class LoanEligibilityTest
{
    private Mock<ILoanEligibility> _loanEligibility;

    [TestMethod]
    public void TestLoanTypePersonal()
    {
        SetMockLoanEligibility();
        //Arrange
        string loanType = "Personal";
        //Act
        bool expected = _loanEligibility.Object.HasCorrectType(loanType);
        //Assert
        Assert.IsTrue(expected);
    }

    [TestMethod]
    public void TestLoanTypeInvalid()
    {
        SetMockLoanEligibility();
        //Arrange
        string loanType = "House";
        //Act
        bool expected = _loanEligibility.Object.HasCorrectType(loanType);
        //Assert
        Assert.IsFalse(expected);
    }

    public void SetMockLoanEligibility()
    {
        _loanEligibility = new Mock<ILoanEligibility>();
        _loanEligibility.Setup(loanElg => loanElg.HasCorrectType("Personal"))
                        .Returns(true);
    }
}

But now I am confused. Since we are not really testing our concrete class but rather its mock, are these unit tests really telling us anything, other than probably that our mocks are working fine?

EN

回答 2

Stack Overflow用户

发布于 2018-09-28 14:20:54

如果我们只公开接口到单元测试,那么这些单元测试如何访问实际的实现以验证它们的行为?

任何你喜欢的方式。

我发现令人满意的一种方法是在抽象类中编写检查,并从扩展抽象类的其他空类的构造函数中传入被测系统的实例。

在很多方面,测试框架都是......好吧......“框架”(显然)......因此将可测试组件视为注入框架的东西是有意义的。请参阅Mark Seemann,了解DI友好框架的外观,并确定您认为这些想法对于您的测试套件是否合理。

可以先用这种方式进行测试,但是我会承认,一些将这些问题分开的动作会让人觉得有点做作 - 尽早引入接口,因为你真的明白哪种API会很舒服使用,也许是可疑的。

(一个答案可能是在投入编写实施检查之前,抽出时间来加速界面。)

票数 0
EN

Stack Overflow用户

发布于 2018-09-28 15:39:19

要回答你的问题 - 你会使用模拟框架,如Moq。

总体思路是接口或抽象类提供“契约”或一组标准化的API,您可以对其进行编码。

这些接口或抽象类的实现可以单独进行单元测试。这不是问题,事实上 - 这就是你应该定期做的事情。

但是,当这些实现是其他对象的依赖时,会出现复杂性。在这方面 - 要对这样一个复杂的对象进行单元测试,首先必须构造依赖项的实现,将该依赖项插入到您正在测试的任何实例中。

这个过程变得相当繁琐,因为随着依赖链的增长 - 代码行为的可变性可能非常复杂。为了简化测试并且能够在复杂依赖链中单元测试多个条件 - 我们使用模拟框架。

模拟提供的是一种使用特定参数(输入/输出,无论它们可能是什么)“伪造”实现的方法,并将这些伪造插入到依赖图中。虽然是 - 你可以模拟具体的对象 - 模拟由接口或抽象类定义的契约要容易得多。

moq框架文档是理解这些概念的一个不错的起点。https://github.com/Moq/moq4/wiki/Quickstart

编辑:

我觉得这有什么意思混淆,所以我想详细说明。

常见的设计模式(称为SOLID)规定一个对象应该做一件事,只做一件事并且做得好。这被称为单一责任原则。

另一个核心概念是对象应该依赖于抽象而不是具体的实现。这个概念被称为依赖倒置原则。

最后 - Liskov替换原则,规定程序中的对象应该可以替换其子类型的实例,而不会改变程序的正确性。换句话说 - 如果您的对象依赖于抽象,那么您可以为这些抽象提供不同的实现(利用继承),而不会从根本上改变应用程序的行为。

其中也巧妙地跳入了开放/封闭原则。IE - 软件实体应该开放以进行扩展,但是关闭以进行修改。(考虑为这些抽象提供不同的实现)。

最后 - 我们有控制反转原理 - 一个复杂的对象不应该负责创建自己的依赖; 其他东西应该负责创建它们,它们应该通过构造函数,方法或属性注入“注入”,无论它们在何处需要。

那么这如何适用于“解耦系统设计”与单元测试?

答案很简单。

假设我们正在编写一个模拟汽车的软件。汽车有车身和车轮,以及各种其他内部组件。为简单起见,我们会说类型的对象Car有一个构造函数,它将四个wheel对象作为参数:

public class Wheel {
   public double Radius { get; set; }
   public double RPM { get; set; }
   public void Spin(){ ... }
   public double GetLinearVelocity() { ... }
}

public class LinearMovement{
   public double Velocity { get; set; }     
}

public class Car {

  private Wheel wheelOne;
  private Wheel wheelTwo;
  private Wheel wheelThree;
  private Wheel wheelFour;

  public Car(Wheel one, Wheel two, Wheel three, Wheel four){
    wheelOne = one;
    wheelTwo = two;
    wheelThree = three;
    wheelFour = four;
  } 

  public LinearMovement Move(){
    wheelOne.Spin();
    wheelTwo.Spin();
    wheelThree.Spin();
    wheelFour.Spin();

    speedOne = wheelOne.GetLinearVelocity();
    speedTwo = wheelTwo.GetLinearVelocity();
    speedThree = wheelThree.GetLinearVelocity();
    speedFour = wheelFour.GetLinearVelocity();

    return new LinearMovement(){ 
       Velocity = (speedOne + speedTwo + speedThree + speedFour) / 4
    };
  }
}

汽车行驶的能力取决于汽车的车轮类型。车轮可以具有柔软的橡胶,从而将汽车粘合到拐角处的道路上,或者对于深雪而言可以非常窄但速度非常慢。

因此 - 轮子的想法成为一种抽象。那里有各种各样的轮子,轮子的具体实现不可能涵盖所有轮子。输入依赖性反转原则。

我们使用IWheel界面制作轮子抽象,以宣告任何轮子应该能够做的基本最小功能,以便与我们的汽车一起工作。(在我们的例子中,它应至少旋转......)

public interface IWheel {
    double Radius { get; set; }
    double RPM { get; set; }
    void Spin();
    double GetLinearVelocity();
}

public class BasicWheel : IWheel {
   public double Radius { get; set; }
   public double RPM { get; set; }
   public void Spin(){ ... }
   public double GetLinearVelocity() { ... }   
}

public class Car {
    ...
    public Car(IWheel one, IWheel two, IWheel three, IWheel four){
    ...
    } 

    public LinearMovement Move(){
        wheelOne.Spin();
        wheelTwo.Spin();
        wheelThree.Spin();
        wheelFour.Spin();

        speedOne = wheelOne.GetLinearVelocity();
        speedTwo = wheelTwo.GetLinearVelocity();
        speedThree = wheelThree.GetLinearVelocity();
        speedFour = wheelFour.GetLinearVelocity();

        return new LinearMovement(){ 
            Velocity = (speedOne + speedTwo + speedThree + speedFour) / 4
        };
    }
}

所以这很好,我们得到了一个抽象来定义一个轮子的基本功能,我们编码汽车反对这个抽象。汽车如何移动的代码没有任何改变 - 从而满足Liskov替代原则。

所以现在如果不是创造一辆带有基本车轮的汽车,而是用RacingPerformanceWheels创造一辆汽车,那么控制汽车行驶方式的代码将保持不变。这满足开放/封闭原则。

然而 - 它带来了另一个问题。汽车的实际速度 - 取决于所有4个车轮的平均线速度。因此,根据车轮 - 汽车的行为会有所不同。

考虑到可能有一百万种不同类型的车轮,我们如何测试汽车的行为?!?

进入模拟框架。因为汽车的运动取决于界面定义的车轮的抽象概念IWheel- 我们现在可以模拟这种车轮的不同实施方式,每个都有预定义的参数。

具体的轮子实现/对象本身(BasicWheelRacingPerformanceWheel等等)应该在没有模拟的情况下进行单元测试。原因是他们没有自己的依赖。如果轮子在它的构造函数中有依赖关系 - 那么应该使用模拟来实现该依赖关系。

要测试汽车对象 - 应该使用模拟来描述IWheel传递给汽车构造函数的每个实例(依赖项)。这提供了一些优点 - 将整个系统设计与单元测试分离:

1)我们不关心系统中的轮子是什么。可能有100万。

2)我们关心的是,对于特定的车轮尺寸,在给定的角速度(RPM)下,汽车应该达到非常特定的线速度。

IWheel对于#2的要求的模拟将告诉我们我们的车辆是否正常工作,如果没有 - 我们可以更改我们的代码以纠正错误。

票数 0
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/-100005042

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档