【问题标题】:FakeItEasy Action parameter in UnitTest, but still execute inner Action codeUnitTest中的FakeItEasy Action参数,但仍然执行内部Action代码
【发布时间】:2015-12-08 18:42:44
【问题描述】:

我目前正在为我添加到我们的 ASP.NET 项目中的一些新功能制作一些单元测试(不,这不是测试驱动设计)。我们使用 NHibernate 框架并使用 UnitTest Mock-ing 库FakeItEasy

我有以下要测试的类和方法:

public class Round
{
    public static Round Create(List<Company> activeCompanies, Period period,
                               BusinessUser user, BusinessUser systemUser, 
                               ISession session, IEntityQuery entityQuery,
                               RoundProcessBuilder processBuilder)
    {
        var round = new Round
        {
            Processes = new List<Process>();
            Period = period,
            CreationDate = DateTime.Now,
            CreatedBy = user
        };

        // Save the Round in the DB so we can use it's Id in the Processes:
        session.Save(round);             

        foreach (var company in activeCompanies)
        {
            var companyData = session.Get<CompanyData>(company.Id);
            var processResult = 
                  roundProcessBuilder.Build(
                       systemUser, 
                       new CreateRoundProcessData(company, round, companyData),
                       entityQuery, 
                       session);

            processResult.HandleProcess(process =>
                {
                    // serviceBus can stay null
                    process.Create(systemUser, DateTime.Now, session, null); 
                    // No need to save the session here. If something went 
                    // wrong we don't want halve of the processes being saved
                    round.Processes.Add(process);

                    // It's all or nothing
                });
        }

        return round;
    }
}

我主要要测试的内容:当我对假设 100 家活跃公司使用此 Round#Create 方法时,它应该创建 100 个流程,并且每个流程都应该包含 RoundId

到目前为止,这是我的单元测试:

[TestFixture]
public class RoundTest
{
    private BusinessUser _systemUser;
    private DateTime _creationDateRound1;
    private List<Company> _activeCompanies;
    private RoundProcessBuilder _roundProcessBuilder;
    private ISession _session;

    [SetUp]
    public void Setup()
    {
        _creationDateRound1 = new DateTime(2015, 10, 5);
        _systemUser = TestHelper.CreateBusinessUser(Role.Create("systemuser", "test", 
            Int32.MaxValue));
        _activeCompanies = new List<Company>
        {
            TestHelper.CreateCompany();
        };
        _roundProcessBuilder = A.Fake<RoundProcessBuilder>();
        _session = A.Fake<ISession>();
    }

    [Test]
    public void TestCreateRoundWithoutPreviousRound()
    {
        var fakeExpectedRound = Round.Create(_activeCompanies, DateTime.Now.ToPeriod(),
            _systemUser, _systemUser, _session, null, _roundProcessBuilder);
        var fakeExpectedRoundData = RoundProcessData.Create(TestHelper.CreateCompany(),
            fakeExpectedRound, new CompanyData());
        var fakeExpectedProcess = new Process(_systemUser, null, "processName", null,
            fakeExpectedRoundData, "controllerName", null);
        var processSuccessResult = new ProcessSuccessResult(fakeExpectedProcess);

        A.CallTo(() => _roundProcessBuilder.Build(null, null, null, null))
            .WithAnyArguments()
            .Returns(processSuccessResult);

        A.CallTo(() => processSuccessResult.HandleProcess(A<Action<Process>>.Ignored))
            .Invokes((Action<Process> action) => action(fakeExpectedProcess));
        var round = Round.Create(_activeCompanies, _ceationDateRound1.ToPeriod(),
            _systemUser, _systemUser, _session, null, _roundProcessBuilder);

        Assert.AreEqual(_activeCompanies.Count, round.Processes.Count, "Number of processes");
        Assert.AreEqual(round.Period.Quarter, Math.Ceiling(_creationDateRound1.Month / 3.0m), "Quarter");
        Assert.AreEqual(round.Period.Year, round.Year, "Year");

        // Test if each of the processes knows the RoundId, have the proper state,
        // and are assigned to the systemuser
        //foreach (var process in round.Processes)
        //{
        //    var roundProcessData = process.ProcessData as RoundProcessData;
        //    Assert.IsNotNull(roundProcessData, "All processes should have RoundProcessData-objects as their data-object");
        //    Assert.AreEqual(roundProcessData.Round.Id, round.Id, "RoundId");
        //    Assert.AreEqual(process.Phase.State, PhaseState.Start, "Process state should be Start");
        //    Assert.AreEqual(process.AssignedTo, _systemUser, "AssignedTo should be systemuser");
        //}
    }

    ... // More tests
}

我的问题在于以下代码:

A.CallTo(() => processSuccessResult.HandleProcess(A<Action<Process>>.Ignored))
    .Invokes((Action<Process> action) => action(fakeExpectedProcess));

它给出一个“The specified object is not recognized as a fake object.”错误。

我有这部分代码的原因是因为没有它,下面部分中的process 为空:

processResult.HandleProcess(process => // <- this was null
    {
        process.Create(systemUser, DateTime.Now, session, null);
        round.Processes.Add(process);
    });

PS:我在 UnitTest 中通过额外检查取消了 foreach 的注释,因为当我模拟 process 本身时,它很可能毫无用处。我的主要测试是是否根据活动创建进程并将其添加到列表中给的公司。

【问题讨论】:

    标签: c# unit-testing mocking action fakeiteasy


    【解决方案1】:

    您的问题似乎是您试图将“假”逻辑添加到实际上不是假的对象:

    // You create this as an instance of ProcessSuccessResult:
    var processSuccessResult = new ProcessSuccessResult(fakeExpectedProcess);
    

    ...然后继续尝试在此处添加条件:

    A.CallTo(() => 
         processSuccessResult
             .HandleProcess(A<Action<Process>>.Ignored))
             .Invokes((Action<Process> action) => action(fakeExpectedProcess));
    

    为了完成这最后一点,变量processSuccessResult 需要是接口的假实例,以便 FakeItEasy 可以使用它,并应用您想要的逻辑。

    我假设ProcessSuccessResult 是您可以访问并且能够编辑的课程?如果是这样,您应该能够为其添加一个接口,该接口将包含您需要的方法,因此您可以稍后对其进行处理。

    一旦你定义了它,你应该能够如下创建你的假对象,其中IProcessSuccessResult 将是你的接口的假实现,由 FakeItEasy 提供:

    var processSuccessResult = A.Fake<IProcessSuccessResult>();
    

    现在您应该能够使用A.CallTo(...) 向该假对象添加逻辑。

    当然,这意味着您的类ProcessSuccessResult 的真正实现包含在变量processSuccessResult 中或通过变量调用。如果需要其中的一部分,那么您可以尝试:

    • 添加与其类似的逻辑,或使用 FakeItEasy 的设置代码从假对象调用它(尽管这可能过于复杂),或者:
    • 添加一个单独的变量来包含一个真实类的实例(即两个变量fakeProcessSuccessResultprocessSuccessResult,分别),并使用单独的测试来测试你的这个类的不同方面,以及它的用法。

    如果可能的话,我会推荐后者。

    我希望这足够清楚,并且对您有用。我知道有时会很复杂,找到测试这样的事情的最佳策略。

    【讨论】:

    • 谢谢!我伪造了ProcessSuccessResultHandleProcess,然后它就过去了。我现在确实有一个 NullReferenceException,但这是我自己的错,因为在构造函数/创建方法之一中将 null 作为参数传递。至少我现在可以继续。看到这个解决方案现在已经很清楚了,但是由于我对FakeItEasy 还很陌生(或者一般是在嘲笑),所以我错过了它。
    猜你喜欢
    • 2017-04-24
    • 1970-01-01
    • 2015-12-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多