有没有办法避免在不使用依赖项时将null参数传递给测试中的类?

时间:2014-01-05 17:41:50

标签: c# unit-testing

给出以下课程和考试

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参数传入构造函数?如果被测试的类现在需要一个新的构造函数参数,我需要在此测试中添加另一个null参数,并且不需要使用该依赖项的所有其他测试。

更新

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

我添加了另一个使用模拟的测试。我想避免的情况是在我不需要的时候不得不处理在被测试类的构造函数中添加额外的参数。

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

我越想到这一点,我就越意识到我想用IoC来构建我的具体(被测试的类)。在建议的单元测试中使用DI / IoC容器吗?

6 个答案:

答案 0 :(得分: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
    }
}

我称这个类为“Mock Manager”的原因是因为这也是你实例化你的模拟的地方。然后,当构造函数签名发生更改时,您只需更改创建测试实例的一个方法。

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

答案 1 :(得分:1)

  

我越想到这一点,我就越意识到我想用IoC来构建我的具体(被测试的类)。在建议的单元测试中使用DI / IoC容器吗?

我更喜欢在class under testSetup方法中创建Init,并且没有理由避免这样做。

如果您想使用IoC / DI创建class under test,可以使用AutoMoq(see on GitHubNuGet 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 Team成员Tim 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());
    }
}

答案 2 :(得分:0)

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

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

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

答案 3 :(得分:0)

我已经包含了MSTest和NUnit属性,为您提供了一个示例。

[TestClass, TestFixture]
public class UserServiceFixture
{
    private UserService UserService;

    // will run for each test method
    [TestInitialize, SetUp]
    public void TestInitilize()
    {
        UserService = new UserService(null);
    }

    [Test, TestMethod]
    public void TestMethod1()
    {
        User user = UserService.Get(0);
    }

    [Test, TestMethod]
    public void TestMethod2()
    {
        User user = UserService.Get(1);
    }
}

答案 4 :(得分:0)

不可以强制调用者始终传递非空值。

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

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

How to implement a generic RepositoryFactory?

答案 5 :(得分:0)

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

我不确定我是否记得它的语法,但它很容易。如果我没有误会,那就像是:

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

完整的方法将是:

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