问题描述
我正在实现一个存储库模式查询类并使用 NSubstitute 进行测试.
存储库接口:
public interface IMyRepository { IQueryable<T> Query<T>(Expression<Func<T, bool>> filter) where T : class; }
DateTimeProvider 接口:
public interface IMyDateTimeProvider { DateTime GetDateNow(); }
应用界面:
public interface IMyApplication { List<Thing> GetThingsByQuery(int status); }
应用实现:
public class MyApplication : IMyApplication { private readonly IMyRepository myRepository; private readonly IMyDateTimeProvider myDateTimeProvider; public MyApplication(IMyRepository myRepository, IMyDateTimeProvider myDateTimeProvider) { this.myRepository = myRepository; this.myDateTimeProvider = myDateTimeProvider; } public List<Thing> GetThingsByQuery(int status) { var createdDate = this.myDateTimeProvider.GetDateNow(); return this.myRepository.Query<Thing>(t => t.CreatedDate == createdDate && t.Status == status).ToList(); } }
测试:
[TestClass] public class ApplicationTest { private IMyApplication myApplication; private IMyDateTimeProvider myDateTimeProvider; private IMyRepository myRepository; [TestMethod] public void QueriesRepository() { // Arrange var createdDate = new DateTime(2014, 1, 1); this.myDateTimeProvider.GetDateNow().Returns(createdDate); const int Status = 1; // Act this.myApplication.GetThingsByQuery(Status); // Assert this.myRepository.Received().Query<Thing>(t => t.CreatedDate == createdDate && t.Status == Status); } [TestInitialize] public void TestInitialize() { this.myRepository = Substitute.For<IMyRepository>(); this.myDateTimeProvider = Substitute.For<IMyDateTimeProvider>(); this.myApplication = new MyApplication(this.myRepository, this.myDateTimeProvider); } }
但测试失败并显示以下消息:
NSubstitute.Exceptions.ReceivedCallsException: Expected to receive a call matching: Query<Thing>(t => ((t.CreatedDate == value(MySolution.Test.ApplicationTest+<>c__DisplayClass0).createdDate) AndAlso (t.Status == 1))) Actually received no matching calls. Received 1 non-matching call (non-matching arguments indicated with '*' characters): Query<Thing>(*t => ((t.CreatedDate == value(MySolution.Application.MyApplication+<>c__DisplayClass0).createdDate) AndAlso (t.Status == value(MySolution.Application.MyApplication+<>c__DisplayClass0).status))*)
DateTime 和 Status 被解析为 value(),这在 Application 和 Test 之间是不同的.
这是为什么?我该如何解决这个问题?
推荐答案
对于复杂的表达式,如果经常发现使用 回调 比使用 Received().一个(不完整的)例子:
Expression<Func<Thing, bool>> receivedFilter receivedFilter = null; myRepository.When(x => x.Query<Thing>(Arg.Any<...>)) .Do(x => receivedQuery = x.Arg<Expression<Func<Thing, bool>>>());
然后,对捕获的过滤器表达式进行断言.只执行表达式的过滤器函数实际上可能更简单(参见例如 这里)
Func<Thing, bool> predicate = receivedFilter.Compile(); var matchingThing = new Thing { CreatedDate = createdData, Status = Status }; // assert matching predicate(matchingThing).Should().BeTrue(); // assert non.matching predicate(nonMatchingThing).Should().BeFalse();
这种方法似乎使测试更加黑盒化,但这通常不是一件坏事.
问题描述
I am implementing a repository pattern Query class and testing using NSubstitute.
Repository interface:
public interface IMyRepository { IQueryable<T> Query<T>(Expression<Func<T, bool>> filter) where T : class; }
DateTimeProvider interface:
public interface IMyDateTimeProvider { DateTime GetDateNow(); }
Application interface:
public interface IMyApplication { List<Thing> GetThingsByQuery(int status); }
Application implementation:
public class MyApplication : IMyApplication { private readonly IMyRepository myRepository; private readonly IMyDateTimeProvider myDateTimeProvider; public MyApplication(IMyRepository myRepository, IMyDateTimeProvider myDateTimeProvider) { this.myRepository = myRepository; this.myDateTimeProvider = myDateTimeProvider; } public List<Thing> GetThingsByQuery(int status) { var createdDate = this.myDateTimeProvider.GetDateNow(); return this.myRepository.Query<Thing>(t => t.CreatedDate == createdDate && t.Status == status).ToList(); } }
Test:
[TestClass] public class ApplicationTest { private IMyApplication myApplication; private IMyDateTimeProvider myDateTimeProvider; private IMyRepository myRepository; [TestMethod] public void QueriesRepository() { // Arrange var createdDate = new DateTime(2014, 1, 1); this.myDateTimeProvider.GetDateNow().Returns(createdDate); const int Status = 1; // Act this.myApplication.GetThingsByQuery(Status); // Assert this.myRepository.Received().Query<Thing>(t => t.CreatedDate == createdDate && t.Status == Status); } [TestInitialize] public void TestInitialize() { this.myRepository = Substitute.For<IMyRepository>(); this.myDateTimeProvider = Substitute.For<IMyDateTimeProvider>(); this.myApplication = new MyApplication(this.myRepository, this.myDateTimeProvider); } }
But the test fails with the following message:
NSubstitute.Exceptions.ReceivedCallsException: Expected to receive a call matching: Query<Thing>(t => ((t.CreatedDate == value(MySolution.Test.ApplicationTest+<>c__DisplayClass0).createdDate) AndAlso (t.Status == 1))) Actually received no matching calls. Received 1 non-matching call (non-matching arguments indicated with '*' characters): Query<Thing>(*t => ((t.CreatedDate == value(MySolution.Application.MyApplication+<>c__DisplayClass0).createdDate) AndAlso (t.Status == value(MySolution.Application.MyApplication+<>c__DisplayClass0).status))*)
The DateTime and Status are being parsed into value() which are different between the Application and the Test.
Why is this? How can I fix this?
推荐答案
For complicate expressions if often find it easier to assert on captured arguments by using callbacks than with Received(). An (incomplete) example:
Expression<Func<Thing, bool>> receivedFilter receivedFilter = null; myRepository.When(x => x.Query<Thing>(Arg.Any<...>)) .Do(x => receivedQuery = x.Arg<Expression<Func<Thing, bool>>>());
Then, assert on the captured filter expression. It might actually simpler to just execute the expression's filter func (see e.g. here)
Func<Thing, bool> predicate = receivedFilter.Compile(); var matchingThing = new Thing { CreatedDate = createdData, Status = Status }; // assert matching predicate(matchingThing).Should().BeTrue(); // assert non.matching predicate(nonMatchingThing).Should().BeFalse();
This approach seems to make the test a little more black-boxed but this is in general not a bad thing.