【问题标题】:Capturing method parameter in jMock to pass to a stubbed implementation在 jMock 中捕获方法参数以传递给存根实现
【发布时间】:2012-08-31 07:50:24
【问题描述】:

我希望实现以下行为。 我的测试类依赖于其他类,我希望用 jMock 模拟这种依赖。大多数方法会返回一些标准值,但有一种方法,我希望调用存根实现,我知道我可以从 will(...) 调用此方法,但我希望该方法由传递给模拟方法的参数完全相同。

测试

@Test
public void MyTest(){
    Mockery context = new Mockery() {
        {
            setImposteriser(ClassImposteriser.INSTANCE);
        }
    };
    IDependency mockObject = context.mock(IDependency.class);
    Expectations exp = new Expectations() {         
        {
            allowing(mockObject).methodToInvoke(????);
            will(stubMethodToBeInvokedInstead(????));
        }       
    };      
}

界面

public interface IDependency {
    public int methodToInvoke(int arg);
}

要调用的方法

public int stubMethodToBeInvokedInstead(int arg){
    return arg;
}

那么如何捕获传递给被模拟方法的参数,以便将它们传递给存根方法呢?

编辑

再举一个例子,假设我希望在以下 (C#) 代码中模拟 INameSource 依赖项,以测试 Speaker 类

public class Speaker
{
  private readonly string firstName;
  private readonly string surname;
  private INameSource nameSource ;
 public Speaker(string firstName, string surname, INameSource nameSource)
  {
    this.firstName = firstName;
    this.surname = surname;
    this.nameSource = nameSource;
  }
  public string Introduce()
  {
    string name = nameSource.CreateName(firstName, surname);
    return string.Format("Hi, my name is {0}", name);
  }
}
public interface INameSource
{
  string CreateName(string firstName, string surname);
}

This is how it can be done in Rhino Mocks for C#我知道这不可能这么简单,因为 Java 中缺少委托

【问题讨论】:

    标签: java unit-testing mocking jmock


    【解决方案1】:

    Duncan 的解决方案效果很好,但甚至还有一个更简单的解决方案,无需借助自定义匹配器。只需使用传递给 CustomActions 调用方法的 Invocation 参数。在这个参数上,您可以调用 getParameter(long i) 方法,该方法为您提供调用的值。

    所以不是这个

    return matcher.getLastValue();
    

    使用这个

    return (Integer) invocation.getParameter(0);
    

    现在您不再需要 StoringMatcher:Duncans 示例现在看起来像这样

    @RunWith(JMock.class)
    public class Example {
    
      private Mockery context = new JUnit4Mockery();
    
      @Test
      public void Test() {
    
        final IDependency mockObject = context.mock(IDependency.class);
    
        context.checking(new Expectations() {
          {
            // No custom matcher required here
            allowing(mockObject).methodToInvoke(with(any(Integer.class)));
    
            // The action will return the first argument of the method invocation.
            will(new CustomAction("returns first arg") {
              @Override
              public Object invoke(Invocation invocation) throws Throwable {
                return (Integer) invocation.getParameter(0);
              }
            });
          }
        });
    
        Integer test1 = 1;
        Integer test2 = 1;
    
        // Confirm the object passed to the mocked method is returned
        Assert.assertEquals((Object) test1, mockObject.methodToInvoke(test1));
        Assert.assertEquals((Object) test2, mockObject.methodToInvoke(test2));
      }
    
      public interface IDependency {
        public int methodToInvoke(int arg);
      }
    

    【讨论】:

    • 很酷的东西与我在 C# 中所做的非常相似。我现在可以简单地写return stubMethodToBeInvokedInstead((Integer) invocation.getParameter(0)); 没有任何自定义匹配器或任何东西,只是一个匿名的内联自定义操作???!!!...在这种情况下非常接近 lambdas.. 试一试后会报告...谢谢
    • 是的!就是这个。我设法使用此策略来部分覆盖默认返回值。我使用 method(r) 来模拟具有不同返回类型(布尔值和 void)的类似方法,但希望默认布尔值为真......如果我能找到时间,我可能应该写一篇关于它的完整帖子。
    • 不错的解决方案,更整洁!
    【解决方案2】:

    和奥古斯托一样,我不认为这是一个好主意。然而,我忍不住玩了一点。我创建了一个自定义匹配器和一个自定义操作,用于存储和返回提供的参数。

    注意:这远非生产就绪代码;我只是玩得很​​开心。这是一个证明解决方案的独立单元测试:

    @RunWith(JMock.class)
    public class Example {
    
      private Mockery context = new JUnit4Mockery();
    
      @Test
      public void Test() {
    
        final StoringMatcher matcher = new StoringMatcher();
        final IDependency mockObject = context.mock(IDependency.class);
    
        context.checking(new Expectations() {
          {
            // The matcher will accept any Integer and store it
            allowing(mockObject).methodToInvoke(with(matcher));
    
            // The action will pop the last object used and return it.
            will(new CustomAction("returns previous arg") {
              @Override
              public Object invoke(Invocation invocation) throws Throwable {
                return matcher.getLastValue();
              }
            });
          }
        });
    
        Integer test1 = 1;
        Integer test2 = 1;
    
        // Confirm the object passed to the mocked method is returned
        Assert.assertEquals((Object) test1, mockObject.methodToInvoke(test1));
        Assert.assertEquals((Object) test2, mockObject.methodToInvoke(test2));
      }
    
      public interface IDependency {
        public int methodToInvoke(int arg);
      }
    
      private static class StoringMatcher extends BaseMatcher<Integer> {
    
        private final List<Integer> objects = new ArrayList<Integer>();
    
        @Override
        public boolean matches(Object item) {
          if (item instanceof Integer) {
            objects.add((Integer) item);
            return true;
          }
    
          return false;
        }
    
        @Override
        public void describeTo(Description description) {
          description.appendText("any integer");
        }
    
        public Integer getLastValue() {
          return objects.remove(0);
        }
      }
    }
    

    更好的计划

    现在您已经提供了一个具体的示例,我可以向您展示如何在 Java 中进行测试,而无需求助于我上面的 JMock 黑客技术。

    首先,您发布的一些 Java 版本:

    public class Speaker {
      private final String firstName;
      private final String surname;
      private final NameSource nameSource;
    
      public Speaker(String firstName, String surname, NameSource nameSource) {
        this.firstName = firstName;
        this.surname = surname;
        this.nameSource = nameSource;
      }
    
      public String introduce() {
        String name = nameSource.createName(firstName, surname);
        return String.format("Hi, my name is %s", name);
      }
    }
    
    public interface NameSource {
      String createName(String firstName, String surname);
    }
    
    public class Formal implements NameSource {
      @Override
      public String createName(String firstName, String surname) {
        return String.format("%s %s", firstName, surname);
      }    
    }
    

    然后是一个测试,它会练习课程的所有有用功能,而不是求助于您最初要求的内容。

    @RunWith(JMock.class)
    public class ExampleTest {
    
      private Mockery context = new JUnit4Mockery();
    
      @Test
      public void testFormalName() {
        // I would separately test implementations of NameSource
        Assert.assertEquals("Joe Bloggs", new Formal().createName("Joe", "Bloggs"));
      }
    
      @Test
      public void testSpeaker() {
        // I would then test only the important features of Speaker, namely
        // that it passes the right values to the NameSource and uses the
        // response correctly
        final NameSource nameSource = context.mock(NameSource.class);
        final String firstName = "Foo";
        final String lastName = "Bar";
        final String response = "Blah";
    
        context.checking(new Expectations() {
          {
            // We expect one invocation with the correct params
            oneOf(nameSource).createName(firstName, lastName);
            // We don't care what it returns, we just need to know it
            will(returnValue(response));
          }
        });
    
        Assert.assertEquals(String.format("Hi, my name is %s", response),
            new Speaker(firstName, lastName, nameSource).introduce());
      }
    }
    

    【讨论】:

    • 很好...我想我可以概括一下...最近刚从 C# 迁移到 java,不知道自定义匹配器和操作。我已经更新了这个问题,以展示它在 Rhino Mocks for C# 中的支持方式以及提供其用法的链接
    • @JugalThakkar 我假设您会收到有关答案更改的通知,但我想我还是最好还是联系您。请参阅上面的方法来解决您的问题,而无需诉诸我们认为奇怪的任何东西。
    • 感谢更新的答案,我看到了我所犯的错误,可能是我试图(或至少最终)同时测试两件事。不敢相信我是怎么错过的。我的真实用例比示例要复杂一些,但现在我认为我走在正确的道路上。无论如何,主要目的是找到 Rhino Mock 的 Do(...) 操作的替代方法,它可以让您为模拟对象的给定方法的给定调用传递备用委托。存储匹配器帮助我更好地理解 jMock 的工作方式
    • @JugalThakkar 很好,但请记住,按照我在回答开头提出的建议是不正常的。我敢肯定总有替代方案。如果您发现自己对 JMock 的摆弄太多,请随时问另一个问题。
    【解决方案3】:

    JMock 不支持您的用例(或我在 java 中知道的任何其他模拟框架)。

    我脑子里有个小声音说你正在尝试做的事情并不理想,而且你的单元测试可能太复杂了(也许它测试了太多的代码/逻辑?)。我看到的一个问题是,您不知道这些模拟需要返回哪些值,并且您正在插入其他东西,这可能会使每次运行都无法重现。

    【讨论】:

    • 我不认为我测试太多,可以说这种方法只是将传递的数字乘以一个因子。这个因素是依赖内部的(可能是从配置文件中读取的)。因此,为了避免读取配置文件(或它所做的任何事情),我想通过让我们说返回总是返回传递的数字的 3 倍来模拟该方法。我知道这可能不是最好的例子。我用 rhino mocks 在 C# 中做了类似的测试。
    • 你的例子有一个设计缺陷:从配置文件中读取参数和计算一个值是两个不同的事情,应该由不同的类来完成。我不知道你对dependency inversion 有多熟悉,但我建议你阅读一下。
    • 是的,我知道这些技术。这就是我注入要模拟的依赖项的方式
    • 感谢您指出设计缺陷。但这只是一个例子,如果我可以在这里添加我的真实用例,我会这样做
    • 您可以检查已编辑的问题以了解可能有效的用例以及 C# 中的 Rhino Mock 框架如何支持该用例
    猜你喜欢
    • 2023-03-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-12-12
    • 2011-07-03
    • 2017-09-16
    • 1970-01-01
    相关资源
    最近更新 更多