C# 如何在没有实现的情况下对接口方法进行单元测试

C# How to unit test an interface method without implementation

我是单元测试和 Whosebug 的新手。

我要在下面的界面中测试RefreshAmount:

public interface IAccountService
{
    double GetAccountAmount(int accountId);
}

这是一个依赖于此接口的 class:

public class AccountObj
{
    private readonly int _Id;
    private readonly IService _service;
    public AccountObj(int Id, IService service)
    {
        _Id = Id;
        _service = service;
    }
    public double Amount { get; private set; }
    public void RefreshAmount()
    {
        Amount = _service.GetAmount(_Id);
    }
}

如何对 RefreshAmount 的行为进行单元测试?

RefreshAmount 调用 IService.GetAmount 可能会调用后端办公室,但我没有实现它。任何关于前进道路的建议将不胜感激。 (我已经阅读了最小起订量和依赖注入,但我对单元测试还是个新手)

我不知道 Moq 框架。我们使用 MSTest 和 Microsoft Fakes 可以为您提供存根。

然而,一个天真的直接方法可能是在测试中实现接口 class

public class MyTestImplementation : IAccountService
{
    public bool HasBeenCalled { get; private set; }
    public int ProvidedId { get; private set; }
    public double Amoung { get; set; }

    public double GetAccountAmount(int accountId)
    {
        HasBeenCalled = true;
        ProvidedId = accountId;
    }
}

还有你的测试方法:

[TestMethod] // or whatever attribute your test framework uses
public void TestInterface()
{
    const double EXPECTEDAMOUNT = 341;
    const int EXPECTEDID = 42;
    MyTestImplementation testImpl = new MyTestImplementation();
    testImpl.Amount = EXPECTEDAMOUNT;

    var sut = new AccountObj(EXPECTEDID, testImpl);

    sut.RefreshAmount();

    // use your assertion methods here
    Assert.IsTrue(testImpl.HasBeenCalled);
    Assert.AreEqual(EXPECTEDID, testImpl.ProvidedID);
    Assert.AreEqual(EXPECTEDAMOUNT, sut.Amount);

}

如果您只想检查结果 Amount 是否正确,您可以忽略其他属性。通常你不想检查 如何 RefreshAmount() 做了它应该做的,但前提是结果 Amount 是正确的。

此类事情的单元测试与您 class 的良好组合有关。在这种情况下是。您可以将 "the service" 传递给您的 class,这应该会为您完成工作。

您需要做的是制作 "testServiceClass",它仅用于测试,不会做任何其他事情。

注意:下面的代码缺少所有 "tests" 属性以简要说明代码

namespace MyTests
{
    public class AccountObjTest
    {
        public void Test1()
        {
            int testVal = 10;

            AccountObj obj = new AccountObj(testVal, new AccountObjTestService());
            obj.RefreshAmount();        
            Assert.AreEquals(obj.Amount, testVal);
        }
    }

    internal class AccountObjTestService : IAccountService
    {
        public override double GetAmount(int accountId)
        {
            return accountId;
        }
    }
}

检查 class 本身对您来说很重要 (UNIT-test),而不是跨 (INTEGRATION-test) 的多个 class 的整个实施。

使用最小起订量,这是一个带注释的最小示例测试

[TestClass]
public class AccountObjUnitTests {
    [TestMethod]
    public void AccountObj_Given_Id_RefreshAmount_Should_Return_Expected_Amount() {

        //Arrange
        //creating expected values for test
        var expectedId = 1;
        var expectedAmount = 100D;
        //mock implementation of service using Moq
        var serviceMock = new Mock<IService>();
        //Setup expected behavior
        serviceMock
            .Setup(m => m.GetAmount(expectedId))//the expected method called with provided Id
            .Returns(expectedAmount)//If called as expected what result to return
            .Verifiable();//expected service behavior can be verified

        //the system under test
        var sut = new AccountObj(expectedId, serviceMock.Object);

        //Act
        //exercise method under test
        sut.RefreshAmount();


        //Assert

        //verify that expectations have been met
        serviceMock.Verify(); //verify that mocked service behaved as expected
        Assert.AreEqual(expectedAmount, sut.Amount);
    }

    //Samples class and interface to explain example
    public class AccountObj {
        private readonly int _Id;
        private readonly IService _service;
        public AccountObj(int Id, IService service) {
            _Id = Id;
            _service = service;
        }
        public double Amount { get; private set; }
        public void RefreshAmount() {
            Amount = _service.GetAmount(_Id);
        }
    }

    public interface IService {
        double GetAmount(int accountId);
    }
}

这里是同一测试的更简化版本

[TestMethod]
public void AccountInfo_RefreshAmount_Given_Id_Should_Return_Expected_Amount() {
    //Arrange
    //creating expected values for test
    var expectedId = 1;
    var expectedAmount = 100D;
    //mock implementation of service using Moq with expected behavior
    var serviceMock = Mock.Of<IService>(m => m.GetAmount(expectedId) == expectedAmount);
    //the system under test
    var sut = new AccountObj(expectedId, serviceMock);

    //Act
    sut.RefreshAmount();//exercise method under test

    //Assert
    Assert.AreEqual(expectedAmount, sut.Amount);//verify that expectations have been met
}

我强烈建议您使用最小起订量来实现您想要实现的目标。它将允许您 "Fake" 传递给要测试的 class 的任何接口的实现。这样您一次只能测试一个 class 的行为。这将使单元测试在长期 运行.

中变得容易得多

创建测试 class 似乎是一个简单的选择,但是,如果您要处理大量场景,则该测试 class 将不得不增长并变得更加复杂迎合所有场景,在某些时候你也必须测试它,因为它很复杂。

尝试学习如何使用 moq,它会在长期得到回报 运行。您将能够验证传递给模拟对象的数据,控制模拟行为返回的内容,测试模拟方法是否已被调用以及它们被调用了多少次。最小起订量非常有用。

看看 https://github.com/Moq/moq4/wiki/Quickstart 它向初学者解释了如何很好地使用 Moq

我假设您的代码中是否有一些输入错误,因为您的接口被调用 IAccountService 而您的服务实现 IService。你的 class 被称为 AccountObj 而你的构造函数被称为 AccountInfo.

所以让我们假设您的服务预计将实施 IAccountService 并且您的 class 预计将命名为 AccountInfo.

编写单元测试来测试AccountInfo.RefreshAmount时,您必须了解此功能的要求;你必须确切地知道这个函数应该做什么。

看代码好像RefreshAmount的要求是:

Whatever object that implements IAccountService and whatever Id are used to construct an object of class AccountInfo, the post condition of calling RefreshAmount is that property Amount returns the same value as IAccountService.GetAccountAmount(Id) would have returned.

或更正式:

  • 对于任何 ID
  • 对于实现 IAccountService
  • 的任何 class
  • 使用 Id 和 IAccountService 创建的对象应该在调用 RefreshAmount() 后,return 属性 Amount 的值,等于由 [=61] 编辑的值 return =](Id).

因为要求说它应该适用于所有 ID 和所有 AccountServices,所以您不能用所有这些来测试您的 class。在这种情况下,在编写单元测试时,您必须考虑可能发生的错误。

在你的例子中看起来没有可能的错误,但对于未来的版本,你可能会想到该函数会使用不正确的 Id 调用服务,或者调用不正确的服务,或者忘记保存 return 值在 属性 金额中,或者 属性 金额不是 return 正确的值。

您的 class 的要求指定它应该适用于每个 Id 和每个 IAcocuntService。因此,您可以自由地向要测试的测试对象的构造函数提供任何 Id 和 AccountService,这可能会检测到四个未来错误中的任何一个。

幸运的是,一个简单的 AccountInfo class 就足够了

class AccountService : IAccountService
{
    public double GetAccountAmount(int accountId)
    {
        return 137 * accountId - 472;
    }
}

以下单元测试测试 Amount 是 return由提供的 IAccountService 实现为几个 ID 编辑的金额

无效TestAccountObj_RefreshAmount() { const int ID = 438; IAccountService accountService = new AccountService(); var testObject = new AccountInfo(Id, accountService);

   testObject.RefreshAmount();
   double amount = testObject.Amount;
   double expectedAmount = accountService.GetAccountAmount(Id);
   Assert.AreEqual(expectedAmount, amount);

}

此测试将测试所有提到的四个可能的错误。它找不到的唯一错误是,如果这个不正确的服务 return 完全相同的奇怪计算数字,它会调用不正确的服务。这就是为什么我在服务中放置如此​​奇怪的计算,任何错误调用的服务都不太可能 return 同样的错误。特别是如果您要使用具有各种 ID

的各种 TestObject 进行测试