【问题标题】:Testing domain when using using repository pattern使用存储库模式时测试域
【发布时间】:2012-04-17 17:17:53
【问题描述】:

单元测试 (TDD) 让我大吃一惊。我有一个要测试的基本存储库模式,但我不确定我是否正确地做事。在这个阶段,我正在测试我的域,而不是担心控制器和视图。为了简单起见,这里有一个演示项目。

public class Person
{
    public int PersonID { get; set; }
    public string Name{ get; set; }
}

界面

public interface IPersonRepository
{
    int Add(Person person);
}

混凝土

public class PersonnRepository : IPersonRepository
{

    DBContext ctx = new DBContext();

    public int Add(Person person)
    {
        // New entity
        ctx.People.Add(person);
        ctx.SaveChanges();
        return person.id;

    }
}

我已将 NUnit 和 MOQ 添加到我的测试项目中,并想知道如何正确测试功能。

我不确定这是否正确,但在阅读了一些博客后,我最终创建了一个 FakeRepository,但是如果我基于此进行测试,那如何验证我的实际界面?

public class FakePersonRepository
{

    Dictionary<int, Person> People = new Dictionary<int, Person>();

    public int Add(Person person)
    {
        int id = People.Count + 1;
        People.Add(id, person);
        return id;
    }
}

然后用

测试
    [Test]
    public void Creating_A_Person_Should_Return_The_ID ()
    {

        FakePersonRepository repository = new FakePersonRepository();

        int id = repository.Add(new Person { Name = "Some Name" });

        Assert.IsNotNull(id);

    }

我是否可以在正确的庄园进行测试?

我想在以后测试诸如不传递名称会导致错误等问题。

【问题讨论】:

    标签: asp.net-mvc unit-testing tdd


    【解决方案1】:

    我是否可以在正确的庄园进行测试?

    恐怕你不是。拥有接口的想法是,它允许您解耦使用存储库(例如您的控制器)的其他代码,并能够对其进行单独的单元测试。因此,假设您有以下要进行单元测试的控制器:

    public class PersonController : Controller
    {
        private readonly IPersonRepository _repo;
        public PersonController(IPersonRepository repo)
        {
            _repo = repo;
        }
    
        [HttpPost]
        public ActionResult Create(Person p)
        {
            if (!ModelState.IsValid)
            {
                return View(p);
            }
    
            var id = _repo.Add(p);
            return Json(new { id = id });
        }
    }
    

    请注意控制器如何不依赖于特定的存储库实现。所有需要的是这个存储库实现给定的合同。现在我们可以在单元测试中使用 Moq 之类的模拟框架来提供一个假存储库,并使其按照我们喜欢的方式运行,以便测试 Create 操作中的 2 条可能路径:

    [TestMethod]
    public void PersonsController_Create_Action_Should_Return_View_And_Not_Call_Repository_If_ModelState_Is_Invalid()
    {
        // arrange
        var fakeRepo = new Mock<IPersonRepository>();
        var sut = new PersonController(fakeRepo.Object);
        var p = new Person();
        sut.ModelState.AddModelError("Name", "The name cannot be empty");
        fakeRepo.Setup(x => x.Add(p)).Throws(new Exception("Shouldn't be called."));
    
        // act
        var actual = sut.Create(p);
    
        // assert
        Assert.IsInstanceOfType(actual, typeof(ViewResult));
    }
    
    
    [TestMethod]
    public void PersonsController_Create_Action_Call_Repository()
    {
        // arrange
        var fakeRepo = new Mock<IPersonRepository>();
        var sut = new PersonController(fakeRepo.Object);
        var p = new Person();
        fakeRepo.Setup(x => x.Add(p)).Returns(5).Verifiable();
    
        // act
        var actual = sut.Create(p);
    
        // assert
        Assert.IsInstanceOfType(actual, typeof(JsonResult));
        var jsonResult = (JsonResult)actual;
        var data = new RouteValueDictionary(jsonResult.Data);
        Assert.AreEqual(5, data["id"]);
        fakeRepo.Verify();
    }
    

    【讨论】:

    • 感谢您的回复。我假设我可以在这个阶段测试域(CRUD + 更复杂的服务)而不需要控制器和视图。我想先测试域模型和与之一起工作的接口,以驱动业务元素的设计,然后再担心输出。这不可能吗?
    【解决方案2】:

    您需要通过为其提取接口来使您的 DBContext 可注入:

    public interface IDBContext{
       IList<Person> People {get;}  // I'm guessing at the types
       void SaveChanges();
       //      etc.
    }
    

    然后将其注入到您的具体类中:

    public class PersonRepository : IPersonRepository
    {
    
        IDBContext ctx;
    
        public PersonRepository(IDBContext db) {
           ctx = db;
        }
    
        public int Add(Person person)
        {
            // New entity
            ctx.People.Add(person);
            ctx.SaveChanges();
            return person.id;
    
        }
    }
    

    您的测试将如下所示:

    [Test]
    public void Creating_A_Person_Should_Return_The_ID ()
    {
    
        Mock<IDBContext> mockDbContext = new Mock<IDBContext>();
        // Setup whatever mock values/callbacks you need
    
        PersonRepository repository = new PersonRepository(mockDbContext.Object);
    
        int id = repository.Add(new Person { Name = "Some Name" });
    
        Assert.IsNotNull(id);
    
        // verify that expected calls are made against your mock
        mockDbContext.Verify( db => db.SaveChanges(), Times.Once());
       //...
    

    }

    【讨论】:

    • 也谢谢。我可能会误解或我的例子令人困惑。在这种情况下,dbContext 旨在表示 EF DbContext,这将允许我使用 EF 和数据库。因此,它不会具有您显示的属性。我具体的 PersonRepository 将访问 EF 以写入数据库,据我了解,这表明不是单元测试,而是稍后进行集成测试。
    • 我假设我需要一个内存版本,它实现了相同的接口(将 db 排除在等式之外),然后我想确保这意味着我的测试我们确认了我的方法。虽然我对 MVC、EF Code first 和 TDD 的概念都很陌生,但我觉得我有点迷失了。
    • 你想测试你的具体实现,而不是你的接口。为此,您需要将您的实现与 DBContext 分离,这就是为什么我建议您为其提取一个接口,您可以将其注入到您的 PersonRepository 具体类中。单元测试的关键是确保您的所有依赖项都是可注入的,以便您可以隔离类中的代码。然后,在您的单元测试中,您传入依赖项的模拟版本。在您的情况下,这意味着您必须能够像我上面概述的那样注入您的 DBContext。
    【解决方案3】:

    我个人会考虑为此编写一个“集成测试”,即一个访问真实(ish)数据库的测试,因为您的数据访问层不应包含任何使隔离测试值得的逻辑。

    在这种情况下,您需要启动并运行数据库。这可能是已经在某处设置的开发人员数据库,或者作为测试安排的一部分启动的内存数据库。

    这样做的原因是,我发现 DAL 的(纯)单元测试通常最终证明您可以使用模拟框架等等,但最终不会让您对自己的代码充满信心。

    如果您对单元测试完全陌生,并且手头没有大学来帮助您设置 DAL 测试所需的环境,那么我建议您暂时停止测试 DAL,并专注于业务逻辑,因为是您将获得最大收益的地方,并且可以让您更轻松地了解测试将如何帮助您。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-04-10
      • 2012-09-02
      • 1970-01-01
      • 2019-12-16
      • 1970-01-01
      • 2015-06-15
      • 2017-05-14
      • 1970-01-01
      相关资源
      最近更新 更多