版本依赖
注意:powermockito与 mockito版本容易出现不一致的情况。
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<!-- 7.5.0 不兼容 -->
<version>7.4.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<!--
<artifactId>mockito-inline</artifactId>
-->
<artifactId>mockito-core</artifactId>
<version>3.12.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>2.0.9</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-testng</artifactId>
<version>2.0.9</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
</exclusion>
</exclusions>
</dependency>
业务背景
用户登录模块, 代码按照如下分层。 支持多种模式登录。
- controller
- service
- abstract 抽象实现
- MobileService,EmailService,AccountService 分别处理三种不同登录方式
- mapper
- po
- 其他
- Utils : 静态方法工具类
- PasswordEncoder : 普通加密方法类
代码结构
业务代码
Utils
public interface Utils {
static boolean checkIsEmail(String email) {
if ((email != null) && (!email.isEmpty())) {
return Pattern.matches("^(\\w+([-.][A-Za-z0-9]+)*){3,18}@\\w+([-.][A-Za-z0-9]+)*\\.\\w+([-.][A-Za-z0-9]+)*$", email);
}
return false;
}
static void checkIsMobile(String mobile) {
if ((mobile != null) && (!mobile.isEmpty())) {
boolean match = Pattern.matches("^((13[0-9])|(14[0|5|6|7|9])|(15[0-3])|(15[5-9])|(16[6|7])|(17[2|3|5|6|7|8])|(18[0-9])|(19[1|8|9]))\\d{8}$", mobile);
if(match){
return;
}
}
throw new RuntimeException("is not mobile");
}
}
PasswordEncoder
public class PasswordEncoder {
private static final Logger logger = LoggerFactory.getLogger(PasswordEncoder.class);
public String encryptPassword(String plainText) {
String encryptedText = plainText;
logger.debug("{} encrypted result: {}", plainText, encryptedText);
//todo encode 明文
return encryptedText;
}
}
UserMapper
public interface UserMapper {
User get(String account);
User getByMobile(String mobile);
User getByEmail(String email);
}
XService
接口
public interface ILoginService {
boolean support(String input);
Long login(String input, String password);
}
controller: 修改成员属性、父类属性mock
业务代码
public class UserController {
//mocklist
private List<ILoginService> loginServiceList;
public UserController(List<ILoginService> loginServiceList ){
this.loginServiceList = loginServiceList;
}
public Long login(String input , String password){
ILoginService loginService = loginServiceList.stream().filter(service -> service.support(input)).findFirst().orElseThrow(() -> new RuntimeException("未识别的账号类型"));
return loginService.login(input, password);
}
}
测试
public class UserControllerTest {
@InjectMocks
@Spy
private UserController userController;
@Mock
private List<ILoginService> loginServiceList;
@BeforeClass
public void setUp() throws IllegalAccessException {
MockitoAnnotations.openMocks(this);
}
String input = "a@b.com";
String password = "123456";
@Test
public void testLogin_throw() {
try {
userController.login(input, password);
Assert.fail("expect RuntimeException");
} catch (Exception e) {
Assert.assertTrue(e instanceof RuntimeException);
}
}
@Test
public void testLogin_pass() throws IllegalAccessException {
ILoginService loginService = mock(ILoginService.class);
when(loginService.support(anyString())).thenReturn(true);
when(loginService.login(anyString(), anyString())).thenReturn(99L);
List<ILoginService> list2 = Lists.newArrayList(loginService);
//重新设置mock对象成员变量属性
MemberModifier.field(UserController.class, "loginServiceList").set(userController, list2);
Long userId = userController.login(input, password);
Assert.assertTrue(userId.equals(99L));
}
}
AbstractService: 抽象类mock
业务代码
public abstract class AbstractLoginService implements ILoginService {
private static final Logger logger = LoggerFactory.getLogger(AbstractLoginService.class);
@Resource
private PasswordEncoder passwordEncoder;
protected final ThreadLocal<User> threadLocal = new ThreadLocal<>();
@Override
public Long login(String input, String password) {
logger.info("start to login {}/{}",input,password);
getUserInternal(input);
try{
User user = threadLocal.get();
String encodedPassword = passwordEncoder.encryptPassword(password);
if (user.getPassword().equals(encodedPassword)) {
return user.getId();
}
throw new RuntimeException("用户名密码错误");
}finally {
threadLocal.remove();
}
}
public abstract void getUserInternal(String input);
}
测试
public class AbstractLoginServiceTest {
@InjectMocks
// @Spy -- 无法使用spy ,打桩 抽象类
private AbstractLoginService abstractLoginService;
@Mock
private PasswordEncoder passwordEncoder;
@Mock
protected ThreadLocal<User> threadLocal;
@BeforeClass
public void setUp(){
//mock 抽象类
abstractLoginService = Mockito.mock(AbstractLoginService.class, Mockito.CALLS_REAL_METHODS);
MockitoAnnotations.openMocks(this);
}
@Test
public void testLogin() throws IllegalAccessException {
// mock(AbstractLoginService.class), 故无法使用真实的成员变量threadLocal, 也无法inject,
// 这里必须手动inject
MemberModifier.field(AbstractLoginService.class, "threadLocal").set(abstractLoginService, threadLocal);
//mock void 方法 ,do-nothing
// doNothing().when(threadLocal).remove();
User user = mock(User.class);
when(threadLocal.get()).thenReturn(user);
when(user.getPassword()).thenReturn("abc");
when(passwordEncoder.encryptPassword(anyString())).thenReturn("abc");
abstractLoginService.login("123", "xxx");
Assert.assertTrue(true);
try{
when(passwordEncoder.encryptPassword(anyString())).thenCallRealMethod();
abstractLoginService.login("123", "xxx");
Assert.fail("expect 用户名密码错误");
}catch (Exception e){
Assert.assertTrue(e instanceof RuntimeException);
Assert.assertTrue(e.getMessage().equals("用户名密码错误"));
}
}
}
MailLoginService: 静态方法+普通void()方法mock
业务类
public class MailLoginService extends AbstractLoginService {
private static final Logger logger = LoggerFactory.getLogger(MailLoginService.class);
@Resource
private UserMapper userMapper;
@Override
public boolean support(String email) {
return Utils.checkIsEmail(email);
}
@Override
public void getUserInternal(String email) {
User user = userMapper.getByEmail(email);
threadLocal.set(user);
}
}
测试方法
//@MockPolicy(Slf4jMockPolicy.class)
@PowerMockIgnore({"javax.management.*", "jdk.internal.reflect.*"})
@PrepareForTest(Utils.class)
public class MailLoginServiceTest extends PowerMockTestCase {
@InjectMocks
@Spy
private MailLoginService mailLoginService;
@Mock
private UserMapper userMapper;
@BeforeClass
public void setUp(){
MockitoAnnotations.openMocks(this);
//static mock
PowerMockito.mockStatic(Utils.class);
}
@Test
public void testSupport() {
//static
PowerMockito.when(Utils.checkIsEmail(anyString())).thenReturn(true);
Assert.assertTrue(mailLoginService.support("email"));
PowerMockito.when(Utils.checkIsEmail(anyString())).thenReturn(false);
Assert.assertFalse(mailLoginService.support("email"));
}
@Test
public void testGetUserInternal() throws IllegalAccessException {
User user = mock(User.class);
when(userMapper.getByEmail(anyString())).thenReturn(user);
mailLoginService.getUserInternal("abc");
//修改父类属性 -- 为null
MemberModifier.field(MailLoginService.class, "threadLocal").set(mailLoginService, null);
try{
mailLoginService.getUserInternal("abc");
Assert.fail("expect NPE fail");
}catch (Exception e) {
Assert.assertTrue( e instanceof NullPointerException);
}
}
}
MobileLoginService: static void 方法 mock
业务方法
public class MobileLoginService extends AbstractLoginService {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Resource
private UserMapper userMapper;
@Override
public boolean support(String mobile) {
try{
Utils.checkIsMobile(mobile);
return true;
}catch (Exception e) {
return false;
}
}
@Override
public void getUserInternal(String mobile) {
User user = userMapper.getByMobile(mobile);
threadLocal.set(user);
}
}
测试方法
@PowerMockIgnore({"javax.management.*", "jdk.internal.reflect.*"})
@PrepareForTest(Utils.class)
public class MobileLoginServiceTest extends PowerMockTestCase {
@InjectMocks
@Spy
private MobileLoginService mobileLoginService;
@Mock
private UserMapper userMapper;
@BeforeClass
public void setUp(){
MockitoAnnotations.openMocks(this);
PowerMockito.mockStatic(Utils.class);
}
@Test
public void testSupport() throws Exception {
// mock static方法返回值为void
PowerMockito.doNothing().when(Utils.class,"checkIsMobile",any(String.class));
Utils.checkIsMobile("abc");
Assert.assertTrue(true);
PowerMockito.doThrow(new UnsupportedOperationException("不支持的操作")).when(Utils.class,"checkIsMobile",any(String.class));
try{
Utils.checkIsMobile("abc"); //!!!当 Utils.checkIsMobile(null); 打桩失效。
Assert.fail("expect 不支持的操作");
}catch (Exception e){
Assert.assertTrue(e instanceof UnsupportedOperationException);
}
}
}
其它
mock私有方法
业务方法
public class EchoHelper {
public String hi(String name){
String currentTime = currentTime(); //call私有方法
return currentTime + name;
}
private String currentTime(){
return LocalDateTime.now()+"";
}
}
test
@PowerMockIgnore({"javax.management.*", "jdk.internal.reflect.*"})
@PrepareForTest(EchoHelper.class)
public class EchoHelperTest extends PowerMockTestCase {
@Test
public void testHi() throws Exception {
EchoHelper echoHelper = PowerMockito.spy(new EchoHelper());
PowerMockito.when(echoHelper, "currentTime").thenReturn("123456-----");
Assert.assertEquals(echoHelper.hi("小明"),"123456-----小明");
}
}
修改父类or禁止父类方法
业务方法
public class Father {
public String fatherName(String code) {
//todo
return null;
}
}
public class Son extends Father{
public String name(String code) {
String fatherName = fatherName(code); // call父类方法 --- Son mock时,如何替换或禁止call父类方法?
//todo
return null;
}
}
test
@PowerMockIgnore({"com.sun.org.apache.xerces.*", "javax.xml.*", "org.xml.*", "javax.management.*"})
@PrepareForTest({Father})
public class CommentsServiceImplTest extends PowerMockTestCase {
@Test
public void test111() {
Method method = PowerMockito.method(Son.class, "fatherName",String.class);
// 抑制父类的这个方法执行
PowerMockito.suppress(method);
// 替换掉父类的方法1
// PowerMockito.replace(method).with((proxy,method,args) -> "xxx";);
// 替换掉父类的方法2
PowerMockito.replace(method).with(new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return "xxx";
}
});
}
}
主动抛出异常
@Test
public void test(){
//mapper insert时,模拟异常抛出
when(mapper.insert(any()).thenAnswer((invocation)-> new SQLException());
}