【问题标题】:Unit testing void method that creates a new object创建新对象的单元测试void方法
【发布时间】:2011-08-23 04:31:07
【问题描述】:

我有如下方法:

public void ExecuteSomeCommand()
{
    new MyCommand( someInt, SomeEnum.EnumValue ).Execute();
}

我想测试传递给我正在创建的 ICommand 对象的构造函数的枚举值是否正确。有什么办法可以用 Rhino.Mocks 做到这一点?

【问题讨论】:

  • 如果枚举值意外,构造函数会抛出异常吗?
  • 构造函数本身不会抛出。参数由 Execute 方法传递给通过 NavigationManager 类创建的视图的视图模型。新的视图模型然后将枚举用于某些显示属性,如果它是一个意外的值,它会抛出。

标签: c# unit-testing mvvm rhino-mocks


【解决方案1】:

选项 1:使用接缝

最简单的方法是用接缝重构该方法:

public void ExecuteSomeCommand()
{
    this.CreateCommand(someInt, SomeEnum.EnumValue).Execute();
}

// Your seam
protected virtual ICommand CreateCommand(int someInt, 
    SomeEnum someEnum)
{
    return new MyCommand(someInt, SomeEnum.EnumValue);
}

这样你可以通过扩展这个类来拦截'new'操作符的创建。手动执行此操作时,可能如下所示:

public FakeSomeService : SomeService
{
    public int SomeInt;
    public SomeEnum SomeEnum;

    protected override Command CreateCommand(int someInt, 
        SomeEnum someEnum)
    {
        this.SomeInt = someInt;
        this.SomeEnum = someEnum;
        return new FakeCommand();
    }

    private sealed class FakeCommand : Command
    {
        public override void Execute() { }
    }
}

这个假类可以用在你的测试方法中。


选项 2:分离行为和数据

更好的方法是将数据与行为分开。您的命令同时具有数据(消息)和行为(处理该消息)。如果允许您在代码库中进行此类更改:将其分开,例如通过定义命令和命令处理程序。这是一个例子:

// Define an interface for handling commands
public interface IHandler<TCommand>
{
    void Handle(TCommand command);
}

// Define your specific command
public class MyCommand
{
    public int SomeInt;
    public SomeEnum SomeEnum;
}

// Define your handler for that command
public class MyCommandHandler : IHandler<MyCommand>
{
    public void Handle(MyCommand command)
    {
        // here your old execute logic
    }
}

现在您可以使用依赖注入将处理程序注入您要测试的类。这个类现在看起来像这样:

public class SomeService
{
    private readonly IHandler<MyCommand> handler;

    // Inject a handler here using constructor injection.
    public SomeService(IHandler<MyCommand> handler)
    {
        this.handler = handler;
    }

    public void ExecuteSomeCommand()
    {
        this.handler.Handle(new MyCommand
        {
            SomeInt = someInt,
            SomeEnum = someEnum
        });
    }
}

由于您现在将数据与行为分开,因此很容易创建一个伪造的命令处理程序(或使用 Rhino 模拟创建它)来检查是否向处理程序发送了正确的命令。手动看起来像这样:

public class FakeHandler<TCommand> : IHandler<TCommand>
{
    public TCommand HandledCommand { get; set; }

    public void Handle(TCommand command)
    {
        this.HandledCommand = command;
    }
}

这个假处理程序可以在整个单元测试项目中重复使用。使用此 FakeHandler 的测试可能如下所示:

[TestMethod]
public void SomeTestMethod()
{
    // Arrange
    int expected = 23;

    var handler = new FakeHandler<MyCommand>();

    var service = new SomeService(handler);

    // Act
    service.ExecuteSomeCommand();

    // Assert
    Assert.AreEqual(expected, handler.HandledCommand.SomeInt);
}

将数据与行为分开不仅使您的应用程序更具可测试性。它使您的应用程序对更改更具弹性。例如,可以将横切关注点添加到命令的执行中,而无需对系统中的任何处理程序进行更改。因为IHandler&lt;T&gt; 是一个单一方法的接口,所以很容易编写一个decorator,它可以包装每个处理程序并添加诸如日志记录、审计跟踪、分析、验证、事务处理、容错改进等内容。你可以在this article阅读更多相关信息。

【讨论】:

  • 我同意你的第一个建议。不过,假课程不是必需的。我能够使用 Rhino.Mocks 的 AssertWasCalled(...) 来检查是否将正确的参数传递到 CreateCommand(...) 方法中。谢谢! :-)
  • @Alimbada:谢谢。但不要忘记处理程序。想想这如何简化您的测试和设计。
【解决方案2】:

据我所知没有。我想到的最接近的事情是使用工厂,然后创建该工厂的StrictMock。类似的东西:

readonly ICommandFactory factory;

public Constructor(ICommandFactory factory)
{
    this.factory = factory;
}
public void ExecuteSomeCommand()
{
    factory.Create( someInt, SomeEnum.EnumValue ).Execute();
}

然后,您可以将调用期望寄给Create()

HTH

【讨论】:

  • 如果你这样做,你可能需要每个特定命令的命令工厂。对于 OP,该类需要 IMyCommandFactory。这可能不太理想。使用(构造函数)依赖注入仍然是 +1。
猜你喜欢
  • 2010-12-16
  • 2018-10-30
  • 2013-08-01
  • 2017-12-11
  • 1970-01-01
  • 2019-10-20
  • 2020-03-30
  • 1970-01-01
  • 2017-05-11
相关资源
最近更新 更多