顾老师新作“带着ChatGPT玩转软件开发”将于今年底明年初出版
4.5测试代码调整
4.5.1调整单元测试用例
到此所有的代码均已调试完毕,使用JaCoCo运行所有的单元测试用例,看看测试覆盖率,如图4-4所示。

图4-4 调整前单元测试测试覆盖率
产品代码总的测试覆盖率为87.3%,其中ValidateUser.java的测试覆盖率最高,为100%,而User.java和PasswordRecovery.java的测试覆盖率比较低,分别为34.1和50.5%。下面重新对User.java和PasswordRecovery.java生成一次单元测试用例。
1.调整SendSms.java
注释掉SendSms.java中的调试信息
代码4-78 SendSms.java
…
}catch (TeaException error) {
// 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
// 错误 message
logger.info(error.getMessage());
// 诊断地址
logger.info(error.getData().get("Recommend"));
com.aliyun.teautil.Common.assertAsString(error.message);
return false;
} catch (Exception _error) {
TeaException error = new TeaException(_error.getMessage(), _error);
// 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
// 错误 message
logger.info(error.getMessage());
// 诊断地址
logger.info(error.getData().get("Recommend"));
com.aliyun.teautil.Common.assertAsString(error.message);
return false;
}改为
…
catch(Exception e){
logger.error(e.getMessage());
}
return false;2.调整UserTest.java单元测试用例
提示词
对下面代码生成单元测试用例。
代码4-79 User.java
package ebusiness;
import java.util.regex.Pattern;
public class User{
private int id;
private String username;
private String password;
private String phone;
private String email;
public User(String username,String password){
this.username=username;
this.password=password;
this.phone="13611223344";
this.email="a@126.com";
}
public User(String username,String password,String phone,String email){
this.username=username;
this.password=password;
this.phone=phone;
this.email=email;
}
//设置和获得
public int getId(){
return id;
}
public String getUsername(){
return username;
}
public void setUsername(String username){
String usernamePattern="^[a-zA-Z0-9]{5,20}$";
Pattern pattern=Pattern.compile(usernamePattern);
if (username==null||!pattern.matcher(username).matches()){
throw new IllegalArgumentException("无效的用户账号");
}
this.username=username;
}
public String getPassword(){
return password;
}
public void setPassword(String password){
if (password==null||password.trim().isEmpty()){
throw new IllegalArgumentException("密码不能为空");
}
this.password=password;
}
public String getPhone(){
return phone;
}
public void setPhone(String phone){
String phoneRegex="^1[3-9]\\d{9}$";
Pattern pattern=Pattern.compile(phoneRegex);
if (phone==null||!pattern.matcher(phone).matches()){
throw new IllegalArgumentException("无效的中国手机格式");
}
this.phone=phone;
}
public String getEmail(){
return email;
}
public void setEmail(String email){
String emailRegex="^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$";
Pattern pattern=Pattern.compile(emailRegex);
if (email==null||!pattern.matcher(email).matches()){
throw new IllegalArgumentException("无效的电子邮件格式");
}
this.email=email;
}
}ChatGPT回答
用于测试User类的单元测试用例可以使用JUnit框架来编写。以下是一个包含操作正常情况下和异常情况下的测试用例的示例。
首先,确保你的项目中包含JUnit库。以下是JUnit5的测试用例示。
代码4-80 UserTest.java
package ebusiness;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class UserTest{
private User user;
@BeforeEach
public void setUp(){
user=new User("testUser","password123");
}
@Test
public void testUsernameValid(){
assertEquals("testUser",user.getUsername());
}
@Test
public void testPasswordValid(){
assertEquals("password123",user.getPassword());
}
@Test
public void testPhoneDefault(){
assertEquals("13611223344",user.getPhone());
}
@Test
public void testEmailDefault(){
assertEquals("a@126.com",user.getEmail());
}
@Test
public void testSetUsernameValid(){
user.setUsername("newUser");
assertEquals("newUser",user.getUsername());
}
@Test
public void testSetUsernameInvalid(){
Exception exception=assertThrows(IllegalArgumentException.class,() ->{
user.setUsername("");
});
assertEquals("无效的用户账号",exception.getMessage());
assertThrows(IllegalArgumentException.class,() ->{
user.setUsername("abc");
});
assertThrows(IllegalArgumentException.class,() ->{
user.setUsername(null);
});
}
@Test
public void testSetPasswordValid(){
user.setPassword("newPassword");
assertEquals("newPassword",user.getPassword());
}
@Test
public void testSetPasswordInvalid(){
Exception exception=assertThrows(IllegalArgumentException.class,() ->{
user.setPassword("");
});
assertEquals("密码不能为空",exception.getMessage());
assertThrows(IllegalArgumentException.class,() ->{
user.setPassword(null);
});
}
@Test
public void testSetPhoneValid(){
user.setPhone("13812345678");
assertEquals("13812345678",user.getPhone());
}
@Test
public void testSetPhoneInvalid(){
Exception exception=assertThrows(IllegalArgumentException.class,() ->{
user.setPhone("123456");
});
assertEquals("无效的中国手机格式",exception.getMessage());
assertThrows(IllegalArgumentException.class,() ->{
user.setPhone(null);
});
}
@Test
public void testSetEmailValid(){
user.setEmail("newemail@example.com");
assertEquals("newemail@example.com",user.getEmail());
}
@Test
public void testSetEmailInvalid(){
Exception exception=assertThrows(IllegalArgumentException.class,() ->{
user.setEmail("invalidEmail");
});
assertEquals("无效的电子邮件格式",exception.getMessage());
assertThrows(IllegalArgumentException.class,() ->{
user.setEmail(null);
});
}
}说明。
•@BeforeEach注解在每个测试方法之前运行,确保为每个测试创建一个新的用户对象。
•通过对每个方法的正常和异常情况进行测试,验证User类的字段和方法行为。
•使用assertThrows()方法来验证异常是否按预期抛出,同时检查异常信息是否符合预期。
•请确保您在项目中包含JUnit5的依赖,以便能够运行测试。
接下来调整UserTest.java。
代码4-81 UserTest.java
package ebusiness;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class UserTest{
private User user;
@BeforeEach
public void setUp(){
user=new User("testUser","password123");
}
@Test
public void testUsernameValid(){
Assertions.assertEquals("testUser",user.getUsername());
}
@Test
public void testPasswordValid(){
Assertions.assertEquals("password123",user.getPassword());
}
@Test
public void testPhoneDefault(){
Assertions.assertEquals("13611223344",user.getPhone());
}
@Test
public void testEmailDefault(){
Assertions.assertEquals("a@126.com",user.getEmail());
}
@Test
public void testSetUsernameValid(){
user.setUsername("newUser");
Assertions.assertEquals("newUser",user.getUsername());
}
@Test
public void testSetUsernameInvalid(){
Exception exception=assertThrows(IllegalArgumentException.class,() ->{
user.setUsername("");
});
Assertions.assertEquals("无效的用户账号",exception.getMessage());
Assertions.assertThrows(IllegalArgumentException.class,() ->{
user.setUsername("abc");
});
Assertions.assertThrows(IllegalArgumentException.class,() ->{
user.setUsername(null);
});
}
@Test
public void testSetPasswordValid(){
user.setPassword("newPassword");
Assertions.assertEquals("newPassword",user.getPassword());
}
@Test
public void testSetPasswordInvalid(){
Exception exception=assertThrows(IllegalArgumentException.class,() ->{
user.setPassword("");
});
Assertions.assertEquals("密码不能为空",exception.getMessage());
Assertions.assertThrows(IllegalArgumentException.class,() ->{
user.setPassword(null);
});
}
@Test
public void testSetPhoneValid(){
user.setPhone("13681732596");
Assertions.assertEquals("13681732596",user.getPhone());
}
@Test
public void testSetPhoneInvalid(){
Exception exception=assertThrows(IllegalArgumentException.class,() ->{
user.setPhone("123456");
});
Assertions.assertEquals("无效的中国手机格式",exception.getMessage());
Assertions.assertThrows(IllegalArgumentException.class,() ->{
user.setPhone(null);
});
}
@Test
public void testSetEmailValid(){
user.setEmail("newemail@example.com");
Assertions.assertEquals("newemail@example.com",user.getEmail());
}
@Test
public void testSetEmailInvalid(){
Exception exception=assertThrows(IllegalArgumentException.class,() ->{
user.setEmail("invalidEmail");
});
Assertions.assertEquals("无效的电子邮件格式",exception.getMessage());
Assertions.assertThrows(IllegalArgumentException.class,() ->{
user.setEmail(null);
});
}
@Test
//测试User类的构造函数和get()方法。
public void testUserCreation(){
User user=new User("testUser","testPassword","1234567890","test@example.com");
Assertions.assertEquals("testUser",user.getUsername());
Assertions.assertEquals("testPassword",user.getPassword());
Assertions.assertEquals("1234567890",user.getPhone());
Assertions.assertEquals("test@example.com",user.getEmail());
}
}修改的内容为。
•将assertXXX改为Assertions.assertXXX。
•将以前的测试用例void testUserCreation()加入其中。
运行这个单元测试脚本,测试通过。
3.调整PasswordRecoveryTest.java单元测试用例
提示词
对下面代码生成单元测试用例。
代码4-82 PasswordRecovery.java
package ebusiness;
import java.sql.SQLException;
import java.util.regex.Pattern;
import org.apache.log4j.Logger;
import com.zaxxer.hikari.HikariDataSource;
import demo.SendSms;
public class PasswordRecovery{
private static final Logger logger=Logger.getLogger(PasswordRecovery.class);
Util util=new Util();
public String sendVerificationCode(String contact) throws Exception{
String contactType=getContactType(contact);
if (contactType==null){
return "您输入的手机号或Email格式不正确!";
}
UserRepository mysql=new UserRepository();
try(HikariDataSource dataSource=mysql.dataSource){
String uid=getUserIdByContact(contact, mysql);
if (!uid.matches("\\d+")){
return uid;
}
int uidi=Integer.parseInt(uid);
String code=util.generateRandomString(6, "int");
if (contactType.equals("phone")){
return sendCodeForSMS(contact, uidi, code, mysql, uid);
} else if (contactType.equals("email")){
return sendCodeForEmail(contact, code, uidi,uid, mysql);
}
}
return "0";
}
public String sendCodeForSMS(String contact, int uidi, String code, UserRepository mysql,String uid) throws Exception{
return sendCode(contact,mysql,uidi,uid,code,"SMS");
}
public String sendCodeForEmail(String contact, String code, int uidi,String uid, UserRepository mysql) throws Exception{
return sendCode(contact,mysql,uidi,uid,code,"Email");
}
public String sendCode(String contact,UserRepository mysql,int uidi,String uid, String code,String sign) throws Exception{
boolean judge=false;
if (sign.equals("SMS")){
judge=sendSmsVerificationCode(contact, uidi, code, mysql).equals("验证码已发送至手机");
}else if(sign.equals("Email")){
judge=sendEmailVerificationCode(contact, code).equals("验证码已发送至邮箱");
}
if(judge){
int info=mysql.deleteCodeInDB(uidi);
if (info==1 ||info==0){
//将新的验证码记录放入password
mysql.putCodeInDB(code,uidi);
return uid;
}else{
logger.error("从code删除数据失败");
return "-1";
}
}else{
if (sign.equals("SMS")){
logger.error("短信发送失败!");
}else if(sign.equals("Email")){
logger.error("邮件发送失败!");
}
return "-1";
}
}
//重置密码
public String recoverPassword(String identifyingCode, String newPassword,UserRepository mysql,int uid) throws SQLException{
// 验证验证码
//判断验证码格式是否正确
ValidateUser validateuser=new ValidateUser();
String info_s=validateuser.valiidentifyingCode(identifyingCode);
if (!info_s.equals("验证成功")){
return info_s;
}
//判断验证码是否正确
int verifyCodeInfo=mysql.verifyCode(identifyingCode,uid);
if (verifyCodeInfo!=1){
return "验证码错误,请重新输入!";
}
//判断密码是否散列
if(!validateuser.isHash(newPassword)){
return "密码需要HASH散列";
}
//获取旧密码
String oldPassword=getOldPassword(mysql,uid);
if (!oldPassword.equals("0")){
//将旧密码放入Password表中
int storeOldPasswordInfo=storeOldPassword(oldPassword,mysql,uid);
if (storeOldPasswordInfo==-1){
return "-1";
}
//验证新密码是否使用过
boolean isPasswordUsedInfo=isPasswordUsed(newPassword,uid,mysql);
if (!isPasswordUsedInfo){
return "这个密码以前设置过,请用一个新密码!";
}
//重置密码
int updatePasswordInfo=updatePassword(newPassword,oldPassword,mysql,uid);
if (updatePasswordInfo==-1){
return "-1";
}
}
return "0";
}
public static String getUserIdByContact(String contact,UserRepository mysql) throws SQLException{
String result=mysql.queryForEmailOrPhone(contact);
return result;
}
private String getContactType(String contact){
String phonePattern="^1[3-9]\\d{9}$";
String emailPattern="^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$";
if (Pattern.matches(phonePattern, contact)){
return "phone";
} else if (Pattern.matches(emailPattern, contact)){
return "email";
} else{
return null;
}
}
private String sendSmsVerificationCode(String contact,int uidi, String code, UserRepository userRepository) throws Exception{
SendSms SMSSender=new SendSms();
boolean message=SMSSender.SendSMS(contact,code);
if (message){
if (userRepository.deleteCodeInDB(uidi) >=0){
userRepository.putCodeInDB(code, uidi);
return "验证码已发送至手机";
} else{
logger.error("从code表删除数据失败");
return "-1";
}
} else{
return "短信发送失败";
}
}
public String sendEmailVerificationCode(String contact, String code){
String message="您刚请求的验证码是:" + code + ",请妥善保存";
boolean result=util.SendMail(contact, message);
if (result){
return "验证码已发送至邮箱";
} else{
return "邮箱发送失败";
}
}
private boolean isPasswordUsed(String newPassword,int uid,UserRepository mysql){
// 检查密码是否已使用的逻辑
int info=mysql.verifyPassword(newPassword,uid);
if (info==1){
return false;//返回使用过
}else{
return true;//返回没有使用过
}
}
private String getOldPassword(UserRepository mysql,int uid){
//获取旧密码
String oldPassword=mysql.getPassword(uid);
if ((!oldPassword.equals("0")) &&(!oldPassword.equals("-1"))){
return oldPassword;
}else{
return "0";
}
}
public int storeOldPassword(String oldPassword,UserRepository mysql,int uid) throws SQLException{
//存储旧密码到password表中
int info=mysql.storeOldPassword(oldPassword,uid);
if (info!=1){
logger.error("系统在存储旧密码到password表的时候发生错误");
return -1;
}
return info;
}
public int updatePassword(String newPassword,String oldPassword,UserRepository mysql,int uid) throws SQLException{
//重置密码
int info=mysql.updatePassword(newPassword,uid);
if (info!=1){
logger.error("系统在更新密码的时候发生错误");
return -1;
}
return info;
}
}ChatGPT回答
为了为PasswordRecovery.java生成单元测试用例,首先需要选择一个单元测试框架,比如JUnit。下面的代码段展示了如何为PasswordRecovery类及其主要方法创建单元测试用例。
首先在pom.xml里面加入powermock的反射类。
代码4-83 pom.xml
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-reflect</artifactId>
<version>2.0.9</version>
<scope>test</scope>
</dependency>然后来看一下测试代码。
代码4-84 PasswordRecoveryTest.java
package ebusiness;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.MockedConstruction;
import org.mockito.junit.jupiter.MockitoExtension;
//powermock反射类
import org.powermock.reflect.Whitebox;
import demo.SendSms;
@ExtendWith(MockitoExtension.class)
class PasswordRecoveryTest {
@Mock
private Util mockUtil;
@Mock
private SendSms mockSendSms;
@InjectMocks
private PasswordRecovery passwordRecovery;
UtilTest utiltest = new UtilTest();
private UserRepository mysql;
@Mock
private Connection conn;
@Mock
private PreparedStatement mockPreparedStatement;
@Mock
private ResultSet mockResultSet;
@Mock
private UserRepository userRepository;
@Mock
private Util util;
@Mock
private SendSms SmsSender;
@InjectMocks
private PasswordRecovery service;
@BeforeEach
void setUp() throws SQLException {
conn = mock(Connection.class);
mysql = mock(UserRepository.class);
passwordRecovery = new PasswordRecovery();
MockitoAnnotations.openMocks(this);
Whitebox.setInternalState(passwordRecovery,"util",mockUtil);
}
//测试sendVerificationCode方法
@Test
public void testSendVerificationCode_InvalidContact() throws Exception {
String contact = "invalid_contact";
String result = passwordRecovery.sendVerificationCode(contact);
Assertions.assertEquals("您输入的手机号或Email格式不正确!", result);
}
//测试recoverPassword()方法成功
@Test
public void testRecoverPassword_Success() throws SQLException {
String identifyingCode = "123456";
String newPassword = utiltest.sha256("hashedPassword");
String oldPassword = utiltest.sha256("oldPassword");
int uid = 123;
when(userRepository.verifyCode(identifyingCode, uid)).thenReturn(1);
when(userRepository.getPassword(uid)).thenReturn(oldPassword);
when(passwordRecovery.storeOldPassword(oldPassword, userRepository, uid)).thenReturn(1);
when(passwordRecovery.updatePassword(newPassword,oldPassword,userRepository,uid)).thenReturn(1);
when(userRepository.verifyPassword(newPassword, uid)).thenReturn(0);
String result = passwordRecovery.recoverPassword(identifyingCode, newPassword, userRepository, uid);
Assertions.assertEquals("0", result);
}
//测试recoverPassword(),密码被使用过
@Test
public void testRecoverPassword_PasswordUsed() throws SQLException {
String identifyingCode = "123456";
String newPassword = utiltest.sha256("hashedPassword");
String oldPassword = utiltest.sha256("oldPassword");
int uid = 123;
when(userRepository.verifyCode(identifyingCode, uid)).thenReturn(1);
when(userRepository.getPassword(uid)).thenReturn(oldPassword);
when(passwordRecovery.storeOldPassword(oldPassword, userRepository, uid)).thenReturn(1);
when(userRepository.verifyPassword(newPassword, uid)).thenReturn(1);
String result = passwordRecovery.recoverPassword(identifyingCode, newPassword, userRepository, uid);
Assertions.assertEquals("这个密码以前设置过,请用一个新密码!", result);
}
//测试recoverPassword(),旧密码错误
@Test
public void testRecoverPassword_OldPasswordError() throws SQLException {
String identifyingCode = "123456";
String newPassword = utiltest.sha256("hashedPassword");
int uid = 123;
when(userRepository.verifyCode(identifyingCode, uid)).thenReturn(1);
when(userRepository.getPassword(uid)).thenReturn("-1");
String result = passwordRecovery.recoverPassword(identifyingCode, newPassword, userRepository, uid);
Assertions.assertEquals("0", result);
}
//测试通过Email或手机号生成密码抛出数据库异常
@Test
public void testGetUserIdByContact_throwsSQLException() throws SQLException {
// Arrange
when(userRepository.queryForEmailOrPhone("error_contact"))
.thenThrow(new SQLException("Database error"));
// Act & Assert
Assertions.assertThrows(SQLException.class, () -> {
passwordRecovery.getUserIdByContact("error_contact",userRepository);
});
}
//测试发送验证码的时候,手机号或Email格式不正确
@Test
void sendVerificationCode_InvalidContact_ReturnsErrorMessage() throws Exception {
String result = passwordRecovery.sendVerificationCode("invalid");
Assertions.assertEquals("您输入的手机号或Email格式不正确!", result);
}
//测试通过手机发送验证码成功
@Test
void sendVerificationCode_ValidPhone_Success() throws Exception {
try (MockedConstruction<UserRepository> mockedRepo = mockConstruction(UserRepository.class,
(mock, context) -> {
when(mock.queryForEmailOrPhone("13812345678")).thenReturn("123");
when(mock.deleteCodeInDB(anyInt())).thenReturn(1);
})) {
// 模拟 SendSms 的实例创建
try (MockedConstruction<SendSms> mockedSms = mockConstruction(SendSms.class,
(mock, context) -> {
when(mock.SendSMS(anyString(), anyString())).thenReturn(true);
})) {
when(mockUtil.generateRandomString(6, "int")).thenReturn("123456");
String result = passwordRecovery.sendVerificationCode("13812345678");
Assertions.assertEquals("123", result);
}
}
}
//测试通过发送短信验证码成功
@Test
void sendSmsVerificationCode_Success() throws Exception {
try (MockedConstruction<SendSms> mocked = mockConstruction(SendSms.class,
(mock, context) -> {
when(mock.SendSMS(anyString(), anyString())).thenReturn(true);
})) {
when(mysql.deleteCodeInDB(anyInt())).thenReturn(1);
when(mysql.putCodeInDB(anyString(), anyInt())).thenReturn(1);
String result = passwordRecovery.sendSmsVerificationCode("13812345678", 1, "123456", mysql);
Assertions.assertEquals("验证码已发送至手机", result);
}
}
//测试重置密码,验证验证码和密码成功
@Test
void recoverPassword_ValidCodeAndPassword_ReturnsSuccess() throws SQLException {
UtilTest utiltest = new UtilTest();
when(mysql.verifyCode("123456", 1)).thenReturn(1);
when(mysql.getPassword(1)).thenReturn("oldHash");
when(mysql.storeOldPassword(anyString(), anyInt())).thenReturn(1);
when(mysql.verifyPassword(utiltest.sha256("newHash"), 1)).thenReturn(0);
when(mysql.updatePassword(utiltest.sha256("newHash"), 1)).thenReturn(1);
String result = passwordRecovery.recoverPassword("123456", utiltest.sha256("newHash"),mysql, 1);
Assertions.assertEquals("0", result);
}
//测试获得发送验证码的是手机号码格式
@Test
void getContactType_ValidPhone_ReturnsPhone() {
PasswordRecovery pr = new PasswordRecovery();
String type = pr.getContactType("13812345678");
Assertions.assertEquals("phone", type);
}
//测试发送验证码成功,返回正确的消息
@Test
void sendEmailVerificationCode_Success_ReturnsSuccessMessage() {
when(mockUtil.SendMail(eq("test@example.com"), anyString())).thenReturn(true);
PasswordRecovery pr = new PasswordRecovery();
Whitebox.setInternalState(pr, "util", mockUtil);
String result = pr.sendEmailVerificationCode("test@example.com", "123456");
Assertions.assertEquals("验证码已发送至邮箱", result);
}
}再次运行,得到如图4-5所示。

图4-5 调整后的测试覆盖率
SendSms.java单元测试覆盖率由原来的65.2%提升到88.7%;User.java单元测试覆盖率由原来的34.1%提升到97.6%;PasswordRecovery.java单元测试覆盖率由原来的50.5%提升到79.7%,并且产品代码的单元测试覆盖率也由原来的73.7%提升到81.5%。
4.单元测试分析
单元测试的个数和被测百分比见表4-2所示。
表4-2 单元测试个数分析

顾翔凡言:AI时代,学习已知的未知变得更加容易,重点是如何找到未知的未知,而未知的未知往往是在学习已知的未知的过程中找到的。