以下内容主要针对 JUnit 4,对于一些变动比较大的地方会加入 Junit 5 的内容
JUnit 4 的 POM 依赖
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>test</scope>
</dependency>JUnit 5 的 POM 依赖
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.10.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.10.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.10.3</version>
<scope>test</scope>
</dependency>@Test 用在 public void 的方法上,被标记的 Method 会被自动 Run;该注解可以设置 expected 属性,用于测试 Exception
public class ExceptionTest {
@Test(expected = NullPointerException.class)
public void nullPointerExceptionTest() {
int[] arr = null;
Arrays.sort(arr);
}
}该注解还有一个 timeout 的属性可以设置,用来测试 Performance,如果超时则 Fail
public class PerformanceTest {
@Test(timeout = 100)
public void test() {
int[] arr = {4, 2, 5, 1};
for (int i = 0; i < 1000000; i++) {
arr[1] = i;
Arrays.sort(arr);
}
}
}@Before/@BeforeEach 用在 public void 的方法上,被标记的 Method 会在其他被 @Test 标记的方法执行之前被运行;
@After/@AfterEach 用在 public void 的方法上,被标记的 Method 会在其他被 @Test 标记的方法执行之后被运行;
@BeforeClass/@BeforeAll 用在 public void 的方法上,被标记的 Method 只会在测试运行的最开始执行一次;
@AfterClass/@AfterAll 用在 public void 的方法上,被标记的 Method 只会在测试运行的结束时执行一次;
通过 Annotation 来设置 Parameterized Unit Test method,JUnit 4 和 JUnit 5 的实现方法有很大不同,下面展示 JUnit 4 的方法
@RunWith(Parameterized.class)
public class StringHelperParameterizedTest {
StringHelper helper;
String expectedOutput;
String input;
// 第一个参数对应数组中第一个元素,第二个参数对应数组中第二个元素
public StringHelperParameterizedTest(String expectedOutput, String input) {
this.input = input;
this.expectedOutput = expectedOutput;
}
@Parameterized.Parameters
public static Collection<String[]> parameters() {
String[][] expectedOutputAndInput = {
{"CD", "AACD"},
{"CD", "ACD"},
{"CDEF", "CDEF"},
{"CDAA", "CDAA"},
{"", "AA"},
};
return Arrays.asList(expectedOutputAndInput);
}
@Before
public void setUp() throws Exception {
helper = new StringHelper();
}
@Test
public void testTruncateAInFirst2Positions() {
assertEquals(this.expectedOutput, helper.truncateAInFirst2Positions(this.input));
}
}需要注意的几点:
- Class 上需要标记
RunWith(Parameterized.class) - 准备参数的 Method 上需要标记
@Parameterized.Parameters,且方法必须为static,且返回一个Collection/Array - Constructor 中形参的顺序需要和,准备参数的 Method 中的参数,的顺序对应,或直接使用
@Parameter注解来做标记
这里有一个局限性,就是如果想要测试两个 Method 的话,就会出现两个 Source of Argument
在 JUnit 5 中,有很多更灵活的方法来提供参数
public class MathUtilsTest {
@ParameterizedTest(name = "{index} => add({0}, {1}) = {2}")
@CsvSource({
"1, 1, 2",
"2, 3, 5",
"4, 5, 9",
"-1, -1, -2",
"-2, 2, 0"
})
void testAdd(int a, int b, int expected) {
MathUtils mathUtils = new MathUtils();
assertEquals(expected, mathUtils.add(a, b));
}
}还有更多其他方法来提供参数,比如 @ValueSource,@MethodSource,@CsvFileSource 等,具体参考官方文档
https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests-sources
Suit Test 可以选择 a set of unit test 同时运行,在 JUnit 4 中使用方法如下
@RunWith(Suite.class)
@Suite.SuiteClasses({
ArrayCompareTest.class,
ExceptionTest.class
})
public class AllTest { }在 JUnit 5 中提供了更多了配置 Suit 的方式,并且注解清晰明确
https://junit.org/junit5/docs/current/user-guide/#junit-platform-suite-engine-example
官方文档:
https://javadoc.io/doc/org.mockito/mockito-core/4.11.0/org/mockito/Mockito.html
有关 Stub 的概念,引入 ChatGPT 给出的回复
在单元测试中,stubs 是一种模拟对象,用于提供控制测试环境所需的固定行为或数据。与 mocks 不同,stubs 通常不关注方法的调用情况,而是用于提供预定义的响应或数据。
通过 Mockito 的 mock 方法可以定义某个 method 的具体返回值,下面用一段代码举例
public class User {
private String firstName;
private String lastName;
// Constructor, Getters, Setters...
}
public interface UserRepository {
User findById(int userId);
}
public class UserService {
private UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public String getUserFullName(int userId) {
User user = userRepository.findById(userId);
return user.getFirstName() + " " + user.getLastName();
}
}当我们想测试 UserService 的返回结果的时候
public class UserServiceTest {
// Set fields...SetUp...
@Test
public void testGetUserFullName() {
// 设置 stub 的行为
User user = new User("John", "Doe");
when(userRepository.findById(1)).thenReturn(user);
// 调用被测方法
String fullName = userService.getUserFullName(1);
// 验证结果
assertEquals("John Doe", fullName);
}
}Stub Method 有更多用法,比如 thenReturn 可以进行链式调用,定义某个 Method 第 N 次调用时候返回的 value
@Test
public void ListTest() {
List<Integer> list = mock(List.class, "Integer");
when(list.size()).thenReturn(1).thenReturn(10);
assertEquals(1, list.size());
assertEquals(10, list.size());
}他还可以使用 thenThrow 来抛出异常,thenAnswer 来更加高度的定义被测试方法的行为
在 when method 中,可以手动给予一个 fixed argument,也可以使用 Mockito 提供的 Argument Matcher,比如说 List 中的 get 方法需要一个 Integer 作为 input,假设需求是无论传给 get 任何整数的时候,都 return 一个 “value” 字符串
@Test
public void ListTest() {
List<String> list = mock(List.class);
when(list.get(anyInt())).thenReturn("value");
assertEquals("value", list.get(0));
assertEquals("value", list.get(99));
}Mockito 还提供更多的 Argument Matcher
https://javadoc.io/doc/org.mockito/mockito-core/4.11.0/org/mockito/ArgumentMatchers.html
Verify 可以用来判断某个方法,有没有被正常的调用、调用(至少/至多)几次、或者从来没有被调用过。
比如 List 方法的 add,可能有调用过 list.add(“Hello”),但是没有调用过 list.add(“Bye”) ,就可以通过 Verify 来验证,更多的例子和使用参考官方文档:
https://javadoc.io/doc/org.mockito/mockito-core/4.11.0/org/mockito/Mockito.html#4
它可以用在 Verify 的过程里,获取某个 method 被调用的时候所传给的参数
假如我们以下代码
public class UserController {
private EmailService emailService;
public UserController(EmailService emailService) {
this.emailService = emailService;
}
public void welcomeNewUser(String userEmail) {
String message = "Welcome to our service!";
emailService.sendEmail(userEmail, message);
}
}然后我们想测试 sendEmail 有没有被正确的调用并且获取我们给的参数
public class UserControllerTest {
@Test
public void testWelcomeNewUser() {
// 创建 EmailService 的 mock 对象
EmailService emailService = mock(EmailService.class);
UserController userController = new UserController(emailService);
// 调用方法
userController.welcomeNewUser("test@example.com");
// 捕获参数
ArgumentCaptor<String> recipientCaptor = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<String> messageCaptor = ArgumentCaptor.forClass(String.class);
// 验证 sendEmail 方法被调用并捕获参数
verify(emailService).sendEmail(recipientCaptor.capture(), messageCaptor.capture());
// 断言捕获的参数值
assertEquals("test@example.com", recipientCaptor.getValue());
assertEquals("Welcome to our service!", messageCaptor.getValue());
}
}它还可以 Capture 对象类型的参数,更多使用参考文档
https://javadoc.io/doc/org.mockito/mockito-core/4.11.0/org/mockito/Mockito.html
Hamcrest 提供了更加强大的、灵活的判定规则,比如在 Assert 的时候可以判断 Value 是否大于、小于某个范围;判断集合大小、集合中是否存在某些 Value 等。
Hamcrest 使用 assertThat 方法
@Test
public void testHamcrest() {
// Collection
List<Integer> list = Arrays.asList(101, 102, 103, 104, 105);
assertThat(list, hasSize(5));
assertThat(list, hasItems(101, 102));
assertThat(list, everyItem(greaterThan(100)));
assertThat(list, everyItem(lessThan(110)));
// String
String str = "Hello";
assertThat(str, hasLength(5));
assertThat(str, not(emptyOrNullString()));
// Array
Integer[] arr = {1, 2, 3};
assertThat(arr, arrayWithSize(3));
assertThat(arr, arrayContaining(1, 2, 3));
assertThat(arr, arrayContainingInAnyOrder(2, 3, 1));
}官方 Java API Doc:
https://hamcrest.org/JavaHamcrest/javadoc/2.2/
有四个比较方便的注解:@Mock,@InjectMocks,@Captor,@RunWith(MockitoJUnitRunner.class),这些注解会自动的 Inject 依赖,和 Spring 中的 @Autowire 差不多
@RunWith(MockitoJUnitRunner.class)
public class MockitoAnnotationTest{
@Mock
UserRepository userRepositoryMock;
@InjectMocks // 它会自动找到 UserRepository 的 Bean 并且注入进来,如果有多个相同依赖,可以考虑手动注入
UserService userService;
@Captor
ArgumentCaptor<Integer> userIdArgCaptor
}如果说,又想使用 Parameterized Test 又想使用 Mockito Annotation 的话,可以通过 @Before 注解来实现
@Before
public void setUp() {
try (AutoCloseable mocks = MockitoAnnotations.openMocks(this)) {
// Other initialization code...
} catch (Exception e) {
System.out.println(e.getMessage());
}
}还有一个办法是添加下面这个注解在 Class 的 fileds 中,要求 JUnit 版本在 4.7 及以上
@Rule
public MockitoRule mockitoRule = MockitoJUnit.rule();spy 和 mock 方法都是 Mockito 提供的,mock 说的简单点就是完全的屏蔽掉原来 Class 的 Logic,然后自己重新定义每个 method;但是 spy 会保留原本的 Class 的逻辑的同时,并不影响我们 stub 其中的某个 method,是一种 Partial Mock
@Test
public void spyTest() {
List list = spy(ArrayList.class);
// 保留原有逻辑
list.add(1);
assertThat(list.size(), equalTo(1));
// Stub
when(list.size()).thenReturn(10);
assertThat(list.size(), equalTo(10));
}不太推荐使用 Spy,主要是因为它可能会造成测试的复杂性和可靠性。
Mockito 从 3.4 之后就支持 mock static method 了,之前需要用 PowerMock 这个框架,但是这个框架于两年多之前就停止更新了,并且它似乎与 Mockito 4 之后的版本有冲突
Mockito 中测试静态方法使用方法大致如下
@Test
public void testStaticMethod() {
// 也可以创建 StatiMock 对象之后,在后面手动 close 掉
try (MockedStatic<MyStaticClass> mockedStatic = Mockito.mockStatic(MyStaticClass.class)) {
// 这里本质是传一个 Answer 的实现
staticMock.when(() -> MyStaticClass.staticMethod().thenReturn("mocked result");
assertEquals("mocked result", MyStaticClass.staticMethod());
}
}Mockito 暂时不支持,有新的方案再更新