注意:powermockito与 mockito版本容易出现不一致的情况。
<dependency>
<groupId>org.testnggroupId>
<artifactId>testngartifactId>
<version>7.4.0version>
<scope>testscope>
dependency>
<dependency>
<groupId>org.mockitogroupId>
<artifactId>mockito-coreartifactId>
<version>3.12.4version>
<scope>testscope>
dependency>
<dependency>
<groupId>org.powermockgroupId>
<artifactId>powermock-api-mockito2artifactId>
<version>2.0.9version>
<scope>testscope>
dependency>
<dependency>
<groupId>org.powermockgroupId>
<artifactId>powermock-module-testngartifactId>
<version>2.0.9version>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.testnggroupId>
<artifactId>testngartifactId>
exclusion>
exclusions>
dependency>
用户登录模块, 代码按照如下分层。 支持多种模式登录。

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");
}
}
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;
}
}
public interface UserMapper {
User get(String account);
User getByMobile(String mobile);
User getByEmail(String email);
}
public interface ILoginService {
boolean support(String input);
Long login(String input, String password);
}
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));
}
}
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("用户名密码错误"));
}
}
}
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);
}
}
}
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");
Assert.fail("expect 不支持的操作");
}catch (Exception e){
Assert.assertTrue(e instanceof UnsupportedOperationException);
}
}
}