13

我设计了一个使用存储库模式的应用程序,然后是一个单独的服务层,例如:

public class RegistrationService: IRegistrationService
{
    public void Register(User user)
    {
        IRepository<User> userRepository = new UserRepository();
        // add user, etc
    }
}

如您所见,我在 Register 方法中实例化了我的存储库。现在,当我想编写一些单元测试时,我无法真正使用它并将其替换为假存储库,可以吗?

我不想将存储库添加为类变量(并通过构造函数设置它),因为我认为这会使我的代码“臭”(并非所有方法都需要所有存储库,而且我不需要调用层以了解存储库等)。

建议?

4

7 回答 7

16

您需要使用依赖注入。UserRepository 是 RegistrationService 类的依赖项。为了使您的类可以正确地进行单元测试(即隔离它们的依赖项),您需要“反转”控制您的依赖项创建的内容。目前,您拥有直接控制权,并且正在内部创建它们。只需反转该控件,并允许外部的东西(例如 Castle Windsor 之类的 IoC 容器)注入它们:

public class RegistrationService: IRegistrationService
{
    public RegistrationService(IRepository<User> userRepo)
    {
        m_userRepo = userRepo;
    }

    private IRepository<User> m_userRepo;

    public void Register(User user)
    {
        // add user, etc with m_userRepo
    }
}

我想我忘了补充。一旦你允许你的依赖被注入,你就打开了它们被模拟的可能性。由于您的 RegistrationService 现在将其依赖的用户存储库作为其构造函数的输入,因此您可以创建一个模拟存储库并将其传入,而不是实际的存储库。

于 2009-06-01T17:51:21.353 回答
9

依赖注入可以非常方便地处理这些事情:

public class RegistrationService: IRegistrationService
{
    public void Register(User user)
    {
        this(user, new UserRepository());
    }

    public void Register(User user, IRepository<User> userRepository)
    {
        // add user, etc
    }

}

这样,您就可以使用 UserRepository 作为默认值,并传入自定义(用于测试的 Mock 或 Stub)IRepository 来满足您的任何其他需要。您可能还想更改第二个注册方法的范围,以防您不想将其公开给所有内容。

一个更好的选择是使用 IoC 容器,它可以为您购买比上述示例更加解耦的语法。你可以得到这样的东西:

public class RegistrationService: IRegistrationService
{
    public void Register(User user)
    {
        this(user, IoC.Resolve<IRepository<User>>());
    }

    public void Register(User user, IRepository<User> userRepository)
    {
        // add user, etc
    }

}

那里有许多 IoC 容器,正如其他人的答案所列出的,或者您可以推出自己的.

于 2009-06-01T17:50:46.707 回答
2

你应该做的是使用控制反转或依赖注入。

您的类不应绑定到另一个类的实现,尤其是像这样的跨层,而应将表示用户存储库的接口作为构造函数或属性。然后当它运行时,你提供具体的实现并使用它。您可以构建假货,然后实现存储库接口并在测试时提供它们。

有很多 IOC 容器,Unity、Ninject、Castle Windsor 和 StructureMap 等等。


我只是想明确一点,在您的单元测试期间,我不知道您是否想要实际使用IOC 容器。当然,集成测试,但对于单元测试,只需新建您正在测试的对象并在该过程中提供您的假货。IOC 容器至少在 IMO 那里可以在应用程序或集成测试级别提供帮助,并使那里的配置更容易。

您的单元测试可能应该保持“纯粹”,并且只与您尝试测试的对象一起工作。至少这对我有用。

于 2009-06-01T17:51:03.087 回答
1

只是为了让每个人都说清楚(或者可能不是),IOC 的基本工作方式是你告诉它用 Y 替换 X。所以你要做的就是接受 IRepository(正如其他人所说)作为参数然后在 IOC 中,您会告诉它用 MockedRepository (例如)替换该特定类的 IRepository。

如果您使用允许 web.config 初始化的 IoC,它会使从 dev 到 prod 的过渡非常顺利。您只需在配置文件中修改适当的参数,您就可以上路了 - 无需接触代码。稍后,如果您决定要移动到不同的数据源 - 您只需将其关闭,然后您就可以开始了。

IoC 是一些有趣的东西。

附带说明一下,如果您只想进行单元测试,您也可以在没有 IOC 注入的情况下执行此操作。您可以通过重载构造函数以接受 IRepository 并以这种方式传递您的模拟存储库来做到这一点。

于 2009-06-01T17:56:39.520 回答
1

我正在做一些事情,嗯,目前几乎是逐字逐句。我正在使用 MOQ 来模拟我的 IUserDataRepository 的代理实例。

从测试的角度来看:

//arrange
var mockrepository = new Mock<IUserDataRepository>();
// InsertUser method takes a MembershipUser and a string Role
mockrepository.Setup( repos => repos.InsertUser( It.IsAny<MembershipUser>(), It.IsAny<string>() ) ).AtMostOnce();

//act
var service = new UserService( mockrepository.Object );
var createResult = service.CreateUser( new MembershipUser(), "WebUser" );

//assert
Assert.AreEqual( MembershipCreateUserResult.Success, createResult );
mockrepository.VerifyAll();

MOQ 也可用于引发特定异常,以便我的 UserService 可以在这些情况下进行测试。

然后就像其他人提到的那样,IoC 将处理所需的依赖。在这种情况下,为 UserService 类创建一个真实的存储库。

于 2009-06-02T20:13:05.220 回答
0

您可以使用 IOC 容器,然后注册不同的存储库。

于 2009-06-01T17:50:07.270 回答
0

我的建议仍然是将存储库移动到类参数,并在需要时使用 IoC 容器注入存储库。这样,代码简单且可测试。

于 2009-06-01T17:51:01.200 回答