最新消息:天气越来越冷,记得加一件厚衣裳

PowerMock – 常用类与接口

Java w3sun 2149浏览 0评论

常用类与接口

Answer

配置mock预期结果的通用接口。Answer接口指定了与mock执行交互时返回的预期结果,适用于根据不同参数返回不同值。

/*
 * Copyright (c) 2007 Mockito contributors
 * This program is made available under the terms of the MIT License.
 */
package org.mockito.stubbing;

import org.mockito.invocation.InvocationOnMock;

/**
 * Generic interface to be used for configuring mock's answer. 
 * Answer specifies an action that is executed and a return value that is returned when you interact with the mock.   
 * <p>
 * Example of stubbing a mock with custom answer: 
 * 
 * <pre class="code"><code class="java">
 * when(mock.someMethod(anyString())).thenAnswer(new Answer() {
 *     Object answer(InvocationOnMock invocation) {
 *         Object[] args = invocation.getArguments();
 *         Object mock = invocation.getMock();
 *         return "called with arguments: " + Arrays.toString(args);
 *     }
 * });
 * 
 * //Following prints "called with arguments: [foo]"
 * System.out.println(mock.someMethod("foo"));
 * </code></pre>
 * 
 * @param <T> the type to return.
 */
public interface Answer<T> {
    /**
     * @param invocation the invocation on the mock.
     *
     * @return the value to be returned
     *
     * @throws Throwable the throwable to be thrown
     */
    T answer(InvocationOnMock invocation) throws Throwable;
}

Book

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Book {
  private String name;
  private Double price;
}

BookDao

public class BookDao {
  public String queryByName(String name) {
    throw new UnsupportedOperationException(
        "BookDao queryByName() not operation supported."
    );
  }
}

BookService

@NoArgsConstructor
public class BookService {
  public String findByName(String name) {
    BookDao bookDao = new BookDao();
    return bookDao.queryByName(name);
  }
}

BookServiceTestWithPowerMock

@RunWith(PowerMockRunner.class)
@PrepareForTest({BookService.class,BookDao.class,BookServiceTestWithPowerMock.class})
public class BookServiceTestWithPowerMock {

  @Test
  public void findByName() throws Exception {
    //Mock对象,此处为局部变量,也可以使用 org.mockito.Mock 注解标记来实现
    BookDao bookDao = PowerMockito.mock(BookDao.class);
    //录制行为,在创建BookDao实例时,不管参数如何都返回上述mock的bookDao
    PowerMockito.whenNew(BookDao.class).withAnyArguments().thenReturn(bookDao);
    BookService bookService = new BookService();
    //录制bookDao的行为以便后续进行回放,queryByName返回值为String可以指定返回的内容。
    //由于具体含义为:当bookDao执行queryByName(name)时返回一个指定的值.
    PowerMockito.when(bookDao.queryByName("C++")).thenReturn("Hard");
    String cplusplus = bookService.findByName("C++");
    //断言结果
    Assert.assertEquals("Hard", cplusplus);
    //录制bookDao的行为以便后续进行回放,queryByName返回值为String可以指定返回的内容。
    //由于具体含义为:当bookDao执行queryByName(name)时返回一个指定的值.
    PowerMockito.when(bookDao.queryByName("VB")).thenReturn("Easy");
    String vb = bookService.findByName("VB");
    //断言结果
    Assert.assertEquals("Easy", vb);
    //录制bookDao的行为以便后续进行回放,queryByName返回值为String可以指定返回的内容。
    //由于具体含义为:当bookDao执行queryByName(name)时返回一个指定的值.
    PowerMockito.when(bookDao.queryByName("Scala")).thenReturn("Easy");
    String scala = bookService.findByName("Scala");
    //断言结果
    Assert.assertEquals("Easy", scala);
  }
}

虽然可以顺利执行测试,但是进行多次调用时需要录制多次行为以便后续回放,这其实是很不优雅的。为此Power Mock提供了一个Answer接口用来承接不同的属于同时根据实际情况返回不同结果从而实现一次行为录制产出多个不同的结果。

BookServiceTestWithPowerMock – upgrade

@RunWith(PowerMockRunner.class)
@PrepareForTest({BookService.class,BookDao.class,BookServiceTestWithPowerMock.class})
public class BookServiceTestWithPowerMock {

  @Test
  public void findByNameWithAnswer() throws Exception {
    //Mock对象,此处为局部变量,也可以使用 org.mockito.Mock 注解标记来实现
    BookDao bookDao = PowerMockito.mock(BookDao.class);
    //录制行为,在创建BookDao实例时,不管参数如何都返回上述mock的bookDao
    PowerMockito.whenNew(BookDao.class).withAnyArguments().thenReturn(bookDao);
    //录制行为,同时在调用方法时指定实现了Answer接口的实现类实例
    PowerMockito.when(bookDao.queryByName(Mockito.anyString())).then(new MyAnswers());
    BookService bookService = new BookService();
    //调用findByName方法之前不必再录制特定的行为,因为在Answer接口的实现类中进行了统一处理
    String cplusplus = bookService.findByName("C++");
    Assert.assertEquals("Hard", cplusplus);
    //调用findByName方法之前不必再录制特定的行为,因为在Answer接口的实现类中进行了统一处理
    String vb = bookService.findByName("VB");
    Assert.assertEquals("Easy", vb);
    try {
      String jackson = bookService.findByName("Jackson");
      Assert.fail("Fail to process here is ok.");
    } catch (Exception e) {
      Assert.assertTrue(e instanceof UnsupportedOperationException);
    }
  }

  /**
   * 配置mock预期结果的通用接口。Answer接口指定了与mock执行交互时返回的预期结果。
   */
  public static class MyAnswers implements Answer<String> {
    /**
     *
     * @param invocationOnMock
     * @return
     * @throws Throwable
     */
    @Override
    public String answer(InvocationOnMock invocationOnMock) throws Throwable {
      String ret;
      //因为只有一个参数因此只获取下标为0的元素进行强转即可.
      String argument = (String) invocationOnMock.getArguments()[0];
      switch (argument) {
        case "C++":
          ret = "Hard";
          break;
        case "VB":
        case "SCALA":
          ret = "Easy";
          break;
        default:
          throw new UnsupportedOperationException("Unknown book name.");
      }
      return ret;
    }
  }
}

answer方法接收到的参数类型为InvocationOnMock,进入到该接口的定义中

/*
 * Copyright (c) 2007 Mockito contributors
 * This program is made available under the terms of the MIT License.
 */

package org.mockito.invocation;

import java.io.Serializable;
import java.lang.reflect.Method;

/**
 * An invocation on a mock
 * <p>
 * A placeholder for mock, the method that was called and the arguments that were passed.
 */
public interface InvocationOnMock extends Serializable {

    /**
     * 获取被mock之后的对象 
     * 
     * @return mock object
     */
    Object getMock();

    /**
     * 获取java.lang.reflect.Method对象,表示被调用了的真实方法
     * 
     * @return method
     */
    Method getMethod();

    /**
     * 获取mock方法中传递的参数(Object[]数组)
     * 
     * @return arguments
     */
    Object[] getArguments();
    
    /**
    * 获取参数数组中指定下标的参数
    * @param index argument position
    * @param clazz argument type
    * @return casted argument on position
    */
    <T> T getArgumentAt(int index, Class<T> clazz);


    /**
     * 获取调用了该mock接口的真实方法,如果mock的是接口将抛出异常
     * <p>
     * <b>Warning:</b> depending on the real implementation it might throw exceptions  
     *
     * @return whatever the real method returns / throws
     * @throws Throwable in case real method throws 
     */
    Object callRealMethod() throws Throwable;
}

ArgumentMatcher

参数匹配器,适用于根据不同参数返回不同值。关于Book,BookDao和BookService我们沿用Answer接口测试时候的类即可,重点关注下参数匹配器的使用。

BookServiceTestWithPowerMock

//定义使用的Runner类型,同时需要用注释的形式将需要测试的静态方法提供给 PowerMockPrepareForTest可以加在指定的方法上.
@RunWith(PowerMockRunner.class)
@PrepareForTest({BookService.class, BookDao.class})
public class BookServiceTestWithPowerMock {

  @Test
  public void findByName() throws Exception {
    //Mock对象,此处为局部变量,也可以使用 org.mockito.Mock 注解标记来实现
    BookDao bookDao = PowerMockito.mock(BookDao.class);
    //录制行为,在创建BookDao实例时,不管参数如何都返回上述mock的bookDao
    PowerMockito.whenNew(BookDao.class).withAnyArguments().thenReturn(bookDao);
    //录制bookDao的行为以便后续进行回放,queryByName返回值为String可以指定返回的内容。
    //由于具体含义为:当bookDao执行queryByName(name)时返回一个指定的值.
    PowerMockito.when(bookDao.queryByName("C++")).thenReturn("Media");
    BookService bookService = new BookService();
    String cplusplus = bookService.findByName("C++");
    Assert.assertEquals("Media", cplusplus);
    //录制bookDao的行为以便后续进行回放,queryByName返回值为String可以指定返回的内容。
    //由于具体含义为:当bookDao执行queryByName(name)时返回一个指定的值.
    PowerMockito.when(bookDao.queryByName("VB")).thenReturn("Media");
    String vb = bookService.findByName("VB");
    Assert.assertEquals("Media", vb);
    //录制bookDao的行为以便后续进行回放,queryByName返回值为String可以指定返回的内容。
    //由于具体含义为:当bookDao执行queryByName(name)时返回一个指定的值.
    PowerMockito.when(bookDao.queryByName("Scala")).thenReturn("Easy");
    String scala = bookService.findByName("Scala");
    Assert.assertEquals("Easy", scala);
  }
}

进行多次调用时与未使用Answer接口前的示例一样,需要录制多次行为以便后续回放,频繁录制行为是非常繁琐的事情同时代码也不会很美观,为此我们可以选择使用org.mockito.ArgumentMatcher进行扩展。

BookServiceTestWithPowerMock – upgrade

//定义使用的Runner类型,同时需要用注释的形式将需要测试的静态方法提供给 PowerMockPrepareForTest可以加在指定的方法上.
@RunWith(PowerMockRunner.class)
@PrepareForTest({BookService.class, BookDao.class})
public class BookServiceTestWithPowerMock {

  @Test
  public void findByNameWithMatcher() throws Exception {
    //Mock对象,此处为局部变量,也可以使用 org.mockito.Mock 注解标记来实现
    BookDao userDao = PowerMockito.mock(BookDao.class);
    //录制行为,在创建BookDao实例时,不管参数如何都返回上述mock的bookDao
    PowerMockito.whenNew(BookDao.class).withAnyArguments().thenReturn(userDao);
    //录制bookDao的行为以便后续进行回放,queryByName返回值为String可以指定返回的内容。
    //由于具体含义为:当bookDao执行queryByName(name)时,name会传递给MyArgumentMatcher,后者会执行用户编写的
    // 逻辑并最终返回一个boolean值,如果为true则返回thenReturn()指定的值,否则返回null值.此时可以避免频繁的行为录制.
    PowerMockito.when(userDao.queryByName(Mockito.argThat(new MyArgumentMatcher()))).thenReturn("Media");
    BookService userService = new BookService();
    //直接调用指定方法并获取结果
    String cplusplus = userService.findByName("C++");
    //对结果进行断言
    Assert.assertEquals("Media", cplusplus);
    //直接调用指定方法并获取结果
    String vb = userService.findByName("VB");
    //对结果进行断言
    Assert.assertEquals("Media", vb);
    String scala = userService.findByName("Scala");
    Assert.assertEquals("Media", scala);
  }

  /**
   * 允许用户创建一个定制化的参数匹配器.
   */
  public static class MyArgumentMatcher extends ArgumentMatcher<String> {

    @Override
    public boolean matches(Object o) {
      String name = (String) o;
      boolean flag;
      switch (name) {
        case "C++":
        case "VB":
        case "Scala":
          flag = true;
          break;
        default:
          flag = false;
      }
      return flag;
    }
  }
}

Verify

使用verify(mock).someMethod(…)来验证方法的调用,为了增加测试的可维护性,官方不推荐我们过于频繁的在每个测试方法中都使用它,如果要验证Mock 对象的某个方法调用次数,则需给verify 方法传入相关的验证参数,它的调用接口是verify(T mock, VerificationMode mode) 。Verify支持多种类型的验证:
  • 验证someMethod()是否能在指定的100毫秒中执行完毕,verify(mock, timeout(100)).someMethod();
  • 在超时验证的同时可进行调用次数验证,默认次数为1,verify(mock, timeout(100).times(1)).someMethod();
  • 给定的时间内完成执行次数,verify(mock, timeout(100).times(2)).someMethod();
  • 给定的时间内至少执行两次,verify(mock, timeout(100).atLeast(2)).someMethod();
  • timeout也支持自定义的验证模式,verify(mock, new Timeout(100,yourOwnVerificationMode)).someMethod();

Book

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Book {
  private String name;
  private Double price;
}

BookDao

public class BookDao {
  public int count() {
    throw new UnsupportedOperationException("BookDao count() operation not supported.");
  }

  public void update(Book book) {
    throw new UnsupportedOperationException("BookDao update() operation not supported.");
  }

  public void save(Book book) {
    throw new UnsupportedOperationException("BookDao save() operation not supported.");
  }
}

BookService

@Data
@NoArgsConstructor
@AllArgsConstructor
public class BookService {
  private BookDao bookDao;

  public void saveOrUpdate(Book book) {
    if (bookDao.count() > 0) {
      bookDao.update(book);
    } else {
      bookDao.save(book);
    }
  }
}

通过Service层可以看到,saveOrUpdate其实是根据获取到的数据条数来判断是更新数据库操作还是插入操作,在count()返回值确定的情况下if语句只会执行一个方法也就是说我们可以确定哪个方法肯定执行了哪个方法肯定没有执行。

BookServiceTestWithPowerMock

//定义使用的Runner类型,同时需要用注释的形式将需要测试的静态方法提供给 PowerMockPrepareForTest可以加在指定的方法上.
@RunWith(PowerMockRunner.class)
@PrepareForTest({BookService.class,BookDao.class})
public class BookServiceTestWithPowerMock {

  @Test
  public void saveOrUpdate() throws Exception {
    //Mock对象,此处为局部变量,也可以使用 org.mockito.Mock 注解标记来实现
    BookDao bookDao = PowerMockito.mock(BookDao.class);
    //录制bookDao的行为以便后续进行回放,指定count()调用时返回值为0.
    PowerMockito.when(bookDao.count()).thenReturn(0);
    //录制行为,在创建BookDao实例时,不管参数如何都返回上述mock的bookDao
    PowerMockito.whenNew(BookDao.class).withAnyArguments().thenReturn(bookDao);
    //实例化一个Book实例
    Book book = PowerMockito.mock(Book.class);
    BookService bookService = new BookService(bookDao);
    //执行操作
    bookService.saveOrUpdate(book);
    //由于返回值为0,因此执行了插入操作,因此save(book)方法一定会执行
    Mockito.verify(bookDao).save(book);
    //由于返回值为0,因此没有执行更新操作,因此update()方法一定没有执行
    Mockito.verify(bookDao, Mockito.never()).update(book);
  }
}

Spy

Mock 不是真实的对象,它只是用类型的 class 创建了一个虚拟对象,并可以设置对象行为。Spy 是一个真实的对象,但它可以设置对象行为。使用Spy生成的类,所有方法都是真实方法,返回值都是和真实方法一样的,spy对象在未指定处理规则时则会直接调用真实方法。

BookService

@NoArgsConstructor
public class BookService {
  public void run() {
    log();
  }

  public void run(String name) {
    log();
  }

  private void log() {
    System.out.println("Logger is logging...");
  }
}

BookServiceTestWithPowerMock

//定义使用的Runner类型,同时需要用注释的形式将需要测试的静态方法提供给 PowerMockPrepareForTest可以加在指定的方法上.
@RunWith(PowerMockRunner.class)
@PrepareForTest(BookService.class)
public class BookServiceTestWithPowerMock {
  @Test
  public void runDirectly() {
    //不只Dao层可以Mock,Service层也可以直接Mock并调用相应的方法.
    BookService BookService = PowerMockito.mock(BookService.class);
    BookService.run();
  }

  @Test
  public void run() {
    //如果使用spy且不满足断言的情况下将会调用真正的方法,否则调用mock的方法。
    BookService service = PowerMockito.spy(new BookService());
    //录制行为,当调用Service层带有参数"yidian"的run方法时doNothing.
    PowerMockito.doNothing().when(service).run("yidian");
    //传入参数为"yidian"因此符合录制行为,此时使用的mock方法,此时不再打印log方法中的"Logger is logging..."标识.
    service.run("yidian");
    System.out.println("========");
    //传入参数为"yidian.io"时,不符合此前录制行为,此时使用的service真实的run(String name)方法.
    service.run("yidian.io");
  }
}

至此Power Mock概念,局部变量,静态方法,final修饰方法,构造方法,常用接口等常用操作已经介绍完毕,希望在日后Unit Test中可以广泛使用使得UT整洁,功能测试覆盖全面一些 👿 。

转载请注明:雪后西塘 » PowerMock – 常用类与接口

发表我的评论
取消评论

表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址