powermock实战

版本依赖

注意: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());
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值