【问题标题】:Is there a way to avoid passing null parameters to class under test when the dependency are not used?当不使用依赖项时,有没有办法避免将空参数传递给被测类?
【发布时间】:2014-01-06 18:02:05
【问题描述】:

给定以下课程和测试

public class UserService
{
    private IUserRepository _userRepo;

    public UserService(IUserRepository userRepo)
    {
        _userRepo = userRepo;
    }

    public User Get(int id)
    {
        var user = _userRepo.Get(id);
        if(user == null)
            throw new CustomException();

        return user;
    }
}

单元测试:

[Fact]
public void MissingUser_ThrowsException()
{
    // Arrange
    var userService = new UserService(null);

    // Act
    Action result = userService.Get(0);

    // Assert
    result.Throws<CustomException>();
}

[Fact]
public void ExistingUser_ReturnsUser()
{
    // Arrange
    var user = new User()
    {
        Id = 0
    };

    var userRepo = new Mock<IUserRepository>();
    userRepo
        .Setup(m => m.Get(0))
        .Return(user);

    var userService = new UserService(userRepo.Object);

    // Act
    var result = userService.Get(0);

    // Assert
    Assert.Equal(user, result);
}

当我知道在测试中不会调用依赖项时,有没有办法避免将空参数传递给构造函数?如果被测类现在需要一个新的构造函数参数,我需要向这个测试和所有其他不使用该依赖项的测试添加另一个 null 参数。

更新:

我正在使用MoqXUnit。也就是说,我想避免使用设置方法,因为我同意XUnit 的理念。但是,任何模拟框架仍然存在同样的问题。

我添加了另一个使用模拟的测试。我试图避免的情况是,当我不需要时,必须处理向被测类的构造函数添加额外的参数。

如果我要在 UserService 方法使用的 UserService 的构造函数中添加另一个依赖项,我只希望第二个测试在运行时失败。目前,我需要为这两个测试添加另一个参数到 UserService 的 ctor。

我对此想得越多,我就越意识到我想使用 IoC 来构建我的具体(被测类)。是否推荐在单元测试中使用 DI/IoC 容器?

【问题讨论】:

  • 听起来您正在寻找Mock framework
  • 如果模拟框架因您的情况而过度杀戮,您可以只运行一个在每个测试方法之前调用的设置
  • 我用更多细节更新了我的问题。

标签: c# unit-testing


【解决方案1】:

虽然您不使用模拟框架很奇怪,但听起来您的问题与另一个问题有关。将编写许多单元测试,并且被测类的构造函数可能会更改。我想您要问的是,当您更改被测类的构造函数签名时,如何避免更改每个测试的繁忙工作?

避免这种情况的一种方法是使用帮助类来管理测试实例的构造。这是一个简单的例子:

public class UserServcieMockManager
{
    //mock objects here if you're using a mocking framework
    public UserService GetServiceForTesting()
    {
        return new UserService(null); //here's where the mocks would be used
    }
}

我将这个类称为“模拟管理器”的原因是因为这也是您可以实例化模拟的地方。那么当构造函数签名改变时,你只需要改变一个创建测试实例的方法。

这种类型的帮助类也变得非常有用,它可以集中设置逻辑以模拟在测试之间重用的场景。

【讨论】:

    【解决方案2】:

    我对此思考得越多,我就越意识到我想使用 IoC 来构建我的具体(被测类)。是否推荐在单元测试中使用 DI/IoC 容器?

    我更喜欢在SetupInit 方法中创建class under test,我认为没有理由避免这样做。

    如果你想使用 IoC/DI 创建你的class under test,你可以使用 AutoMoq (see on GitHub, NuGet package)。

    有一个用法示例:

    [TestClass]
    public class ServiceConsumerTestWithAutoMoq
    {
        [TestMethod]
        public void DoA()
        {
            //arrange
            var mocker = new AutoMoqer();
            var sut = mocker.Create<ServiceConsumer>();
    
            //act
            sut.DoA();
    
            //assert
            mocker.GetMock<IServiceA>().Verify(it => it.Do(), Times.Once());
            mocker.GetMock<IServiceB>().Verify(it => it.Do(), Times.Never());
        }
    
        [TestMethod]
        public void DoB()
        {
            //arrange
            var mocker = new AutoMoqer();
            var sut = mocker.Create<ServiceConsumer>();
    
            //act
            sut.DoB();
    
            //assert
            mocker.GetMock<IServiceA>().Verify(it => it.Do(), Times.Never());
            mocker.GetMock<IServiceB>().Verify(it => it.Do(), Times.Once());
        }
    }
    
    public interface IServiceConsumer
    {
        void DoA();
        void DoB();
    }
    
    public class ServiceConsumer : IServiceConsumer
    {
        public IServiceA serviceA { get; set; }
        public IServiceB serviceB { get; set; }
    
        public ServiceConsumer(
            IServiceA serviceA,
            IServiceB serviceB)
        {
            this.serviceA = serviceA;
            this.serviceB = serviceB;
        }
    
        public void DoA()
        {
            serviceA.Do();
        }
    
        public void DoB()
        {
            serviceB.Do();
        }
    }
    
    
    public interface IServiceA
    {
        void Do();
    }
    
    public interface IServiceB
    {
        void Do();
    }
    

    还有另一个库Moq.AutoMockerMoq TeamTim Kellogg 的成员开发。

    但我宁愿使用SetupInit 方法来创建class under test

    我将使用代码示例来解决您的问题。

    [TestClass]
    public class ServiceConsumerTestWithInit
    {
        private Mock<IServiceA> serviceAMock;
        private Mock<IServiceB> serviceBMock;
        private IServiceConsumer sut;
    
        [TestInitialize]
        public void Initialize()
        {
            serviceAMock = new Mock<IServiceA>();
            serviceBMock = new Mock<IServiceB>();
            sut = new ServiceConsumer(
                serviceAMock.Object,
                serviceBMock.Object);
        }
    
        [TestMethod]
        public void DoA()
        {
            //act
            sut.DoA();
    
            //assert
            serviceAMock.Verify(it => it.Do(), Times.Once());
            serviceBMock.Verify(it => it.Do(), Times.Never());
        }
    
        [TestMethod]
        public void DoB()
        {
            //act
            sut.DoB();
    
            //assert
            serviceAMock.Verify(it => it.Do(), Times.Never());
            serviceBMock.Verify(it => it.Do(), Times.Once());
        }
    }
    

    【讨论】:

      【解决方案3】:

      我不会传递 null,而是模拟 IUserRepository 并简单地传递模拟对象。

      这样,对 null 的检查不会抛出,例如一个ArgumentException...(如果您要实现这样的检查,这在 DI 代码中非常方便)。

      如果您仍然不希望这样做,请实现另一个空 ctor 进行测试。将其标记为内部并使组件内部对您的测试项目可见。或受保护并具有派生的测试类。

      【讨论】:

        【解决方案4】:

        不,您无法强制调用者始终传递非空值。

        您可以做的最好的事情是使用工厂模式将 IRepositoryFactory 传递给构造函数,这样您将只有 1 个参数,并且只需要检查该参数是否为 null。

        您的 RepositoryFactory 将返回您需要的每个存储库的类型。

        How to implement a generic RepositoryFactory?

        【讨论】:

          【解决方案5】:

          听起来您正在寻找Mock framework
          就个人而言,我更喜欢Rhino Mocks

          我不确定我是否记得它的语法,但它很容易。如果我没记错的话是这样的:

           var mockedUserReop = MockRepository.GenerateMock<IUserRepository >();
          

          完整的方法是:

          [TestInitialize, SetUp]
          public void TestInitilize()
          {
              var mockedUserReop = MockRepository.GenerateMock<IUserRepository >();
              UserService = new UserService(mockedUserReop );
          }
          

          【讨论】:

            猜你喜欢
            • 2020-04-15
            • 1970-01-01
            • 2011-07-28
            • 2019-06-28
            • 2022-10-17
            • 2017-07-15
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多