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

时间:2017-02-16 12:57:00

标签: c# unit-testing dependency-injection moq

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

我必须在以下界面中测试RefreshAmount

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

这是一个依赖于这个界面的类:

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和依赖注入的内容,但我对单元测试很安静)

5 个答案:

答案 0 :(得分:3)

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

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

答案 1 :(得分:2)

我假设你的代码中有一些输入错误,因为当你的服务实现IAccountService时,你的界面被称为IService。当您的构造函数被称为AccountObj时,您的类称为AccountInfo

因此,我们假设您的服务需要实施IAccountService,并且您的班级应该被命名为AccountInfo

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

查看代码似乎RefreshAmount的要求是:

  

无论实现IAccountService的任何对象以及用于构造类AccountInfo的对象的任何Id,调用RefreshAmount的后置条件是属性Amount返回相同的将返回IAccountService.GetAccountAmount(Id)的值。

或更正式:

  • 任何Id
  • 对于实现IAccountService
  • 的任何类
  • 使用Id和IAccountService创建的对象应在调用RefreshAmount()之后返回属性Amount的值,等于IAccountService.GetAccountAmount(Id)返回的值。

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

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

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

幸运的是,一个简单的AccountInfo类就是Suffice

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

以下单元测试测试Amount是所提供的IAccountService实现为多个ID返回的数量

void 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);

}

此测试将测试所有四个可能出现的错误。它无法找到的唯一错误是,如果这个不正确的服务将返回完全相同的奇怪计算数字,它将调用不正确的服务。这就是为什么我在服务中加入如此奇怪的计算,任何错误调用的服务都不太可能返回相同的错误。特别是如果您使用各种Ids的各种TestObject进行测试

答案 2 :(得分:1)

对此类事物的单元测试与您班级的良好构成有关。在这种情况下是。您可以将“服务”传递给您的班级,这应该为您完成工作。

你需要做的是制作“testServiceClass”,它只适用于测试,但不会做任何其他事情。

注意:以下代码缺少所有“测试”属性来简要介绍

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

对于您来说,检查类本身(UNIT-test)非常重要,而不是整个实现多个类(INTEGRATION-test)。

答案 3 :(得分:0)

我不了解Moq框架。我们使用MSTest和Microsoft Fakes来提供存根。

然而,一种天真的直接方式可能是在测试类中实现接口

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正确的情况下才能检查。

答案 4 :(得分:0)

我强烈建议您使用Moq来实现您想要实现的目标。它将允许您“伪造”您要传递给您要测试的类的任何接口的实现。这样,您一次只能测试一个类的行为。从长远来看,这将使单元测试变得更加容易。

创建测试类似乎是一个简单的选择,但是,如果您要处理大量场景,那么测试类将不得不增长并变得更加复杂以满足所有场景,并且在某些时候由于其复杂性,我们也必须对此进行测试。

尝试学习如何使用moq,从长远来看它会得到回报。您将能够验证传递给模拟对象的数据,控制模拟行为返回的内容,测试是否已调用模拟方法以及调用它们的次数。 Moq非常有用。

看一下https://github.com/Moq/moq4/wiki/Quickstart,它解释了如何很好地使用Moq作为begginers