移除耦合,然后模拟单元测试

本文关键字:模拟 单元测试 然后 耦合 | 更新日期: 2023-09-27 18:16:30

这是一个两难的选择。假设我们有两个类

Class A
{
    public int memberValue;
}
interface IB
{
    int fun();
}
Class B : IB
{
    public int fun()
    {
        var a = new A();
        switch(a.memberValue)
        {
            case 1:
                //do something
            break;
            case 2:
                //do something
            break;
        }        
    }
}

现在显示了两个紧密耦合的类。为了测试b.f unfun(),我们需要模拟类A,并为a.p membervalue提供多个值。

由于A对象在B.fun()范围之外的任何地方都不需要,我不明白为什么要通过B的构造函数注入它。如何对fun()方法进行单元测试?

移除耦合,然后模拟单元测试

首先,您可能也应该为A创建一个接口,但如果这只是一个简单的POCO数据类,那么最好将其属性设置为virtual,以允许mock。我认为你有三个选择:

  1. 将A注入B的构造函数,如果它是一个经常使用的类(例如,日志类或其他东西)。然后,您可以在测试中创建一个模拟版本,以检查如何使用A(记住,模拟是用于测试具有依赖关系的行为)。

    public class A : IA { ... }
    public class B : IB
    {
        private readonly A a;
        public B(IA a)
        {
            this.a = a;
        }
        public void Func()
        {
            //... use this.a ...
        }
    }
    [Test]
    public void Func_AHasValue1_DoesAction1()
    {
        Mock<IA> mock = new Mock<IA>();
        mock.Setup(a => a.somevalue).Returns("something");
        B sut = new B(mock.Object);
        sut.Func();
        mock.Verify(m => m.SomethingHappenedToMe());
    }
    
  2. 传递A的方法,如果它是B需要工作的东西(因为它似乎在这里)。您仍然可以创建模拟版本以在测试中使用。这与上面的代码相同,但是mock被传递给方法而不是构造函数。如果A是在运行时生成的一些数据类,而不是具有行为的类,则这是更好的方法。
  3. A创建一个工厂类并将其注入构造函数。更改方法以从工厂获取A实例,并在测试中注入模拟工厂以验证行为。

    public class AFactory : IAFactory
    {
        public IA Create() { ... }
    }
    public class B : IB
    {
        private readonly IAfactory factory;
        public B(IAFactory factory)
        {
            this.factory = factory;
        }
        public void Func()
        {
            IA a = factory.Create();
            //... use this.a ...
        }
    }
    [Test]
    public void Func_AHasValue1_DoesAction1()
    {
        Mock<IAFactory> mockFactory = new Mock<IAFactory>();
        Mock<IA> mock = new Mock<IA>();
        mockFactory.Setup(f => f.Create()).Returns(mock.Object);
        mock.Setup(a => a.somevalue).Returns("something");
        B sut = new B(mockFactory.Object);
        sut.Func();
        mock.Verify(m => m.SomethingHappenedToMe());
    }
    
    • 选项1是可以在没有任何运行时信息(例如日志类)的情况下构建类的标准方法。
    • 选项2更适合于输入只是在运行时生成的数据类(例如,用户填写表单,而您有一个表示表单输入的POCO数据类)。
    • 选项3是更好的,当A是有行为的东西,但不能没有在运行时生成的东西创建。

你可以用几种方法来做到这一点。最简单的可能是创建工厂方法,并在测试中创建继承B并覆盖工厂方法的类。我认为这是Feathers或Gojko Adzic在这种情况下提出的建议(我真的不记得我在谁的书中读过,我想是Feathers的《有效地使用遗留代码》)。示例实现类似于:

class B : IB
{
    public int fun()
    {
        A a = this.CreateA();
        ...
    }
    protected A CreateA() { return new A(); }
}

,在单元测试中:

class BTest : B
{
    protected override A CreateA() { return mockA(); }
}

这是一个非常简单和直接的解决方案,它不限制类级别的耦合,但至少将不同的功能移动到不同的方法中,因此执行某些操作的方法不关心对象创建。

但是你需要仔细考虑它是否真的是你想要的。对于小而短的项目来说,这是可以的。对于更大的或长期的东西,重构代码并从B中移除对A的依赖以使类之间的耦合更少可能是有用的。此外,您所展示的模式开始看起来像Strategy设计模式。也许你不应该注入A,而是策略对象,将知道如何处理您的当前情况?当我看到if阶梯或switch语句时,我总是会想到这一点。