Когда я писал статьи по JUnit, мой коллега спросил меня, а могу ли я привести пример использования продвинутого функционала JUnit, например параметризованных тестов, в дикой природе его коде, c использованием mock объектов, внедрения зависимостей итд. Пришлось показывать 🙂
Я не могу поделиться кодом проекта моего коллеги, равно как и примером теста, но полагаю, что аналогичный по содержанию, хотя и несколько надуманный пример теста с использованием всего и сразу будет полезен многим.
Подготовка
Нам понадобятся пустой maven проект с JUnit, Hamcrest и EasyMock, в проекте так же будет незримо присутствовать Spring — в зависимостях его не будет, но мы предполагаем, что приложение написано с его помощью 🙂
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.version>4.12</junit.version>
<hamcrest.version>1.3</hamcrest.version>
<easymock.version>3.3.1</easymock.version>
</properties>
<dependencies>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>7.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-library</artifactId>
<version>${hamcrest.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymock</artifactId>
<version>${easymock.version}</version>
</dependency>
</dependencies>
|
В качестве тестируемого класса предположим, что мы написали собственный механизм авторизации, который выполняет три проверки:
- Проверяет, что присланный пользователем авторизационный токен верный
- Проверяет, что присланный пользователем токен приложения верный
- Проверяет, что пользователь не превышает ограничение на количество запросов
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
|
/**
* Sample user object.
*/
public class User { }
/**
* User authentication token support service.
*/
public interface TokenService {
/**
* Check, if supplied token is valid
* and returns related User object.
* @param token Token string to validate.
* @return Related user object or null if token is not valid.
*/
User validateToken(String token);
/**
* Extracts application id part from the token
* @param token Token string to process
* @return ApplicationId string
*/
String getApplicationId(String token);
}
/**
* Provides application registration facilities.
*/
public interface ApplicationRegistry {
/**
* Checks, if application id is valid.
* @param applicationId Application id to check.
* @return check result.
*/
Boolean validateApplicationId(String applicationId);
}
/**
* Meters rate of users requests per user.
* Provides mechanisms to check, whether user
* are overusing their request limit or not.
*/
public interface RequestLimitingService {
/**
* Checks, if user is overusing his limit
* with specific app or not.
* @param user User to check.
* @param applicationId Id of request application.
* @return result check. True is for positive answer.
*/
Boolean checkLimit(User user, String applicationId);
/**
* Some users are allowed to overuse resources,
* so we have to check it.
* @param user User to check.
* @return result check. True is for positive answer.
*/
Boolean checkExtendedLimit(User user);
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
@Named
public class AuthorizationService {
private final static Logger log = Logger.getLogger(AuthorizationService.class.getName());
@Named
private TokenService tokenService;
@Named
private ApplicationRegistry registry;
@Named
private RequestLimitingService limitingService;
public final Boolean authorize(final String token) {
User user = tokenService.validateToken(token);
if (user == null) {
return false;
}
String applicationId = tokenService.getApplicationId(token);
if (applicationId == null) {
return false;
}
if (!registry.validateApplicationId(applicationId)) {
return false;
}
// Limiting service is known to be buggy
try {
if (!limitingService.checkLimit(user, applicationId)) {
if (!limitingService.checkExtendedLimit(user)) {
return false;
}
}
} catch (NullPointerException ex) {
log.warning("limiting service failed");
return false;
}
return true;
}
}
|
Обычный тест
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
public class AuthorizationServiceTest {
private static final String TOKEN = "TESTTOKEN";
private static final String APP_ID = "TESTAPP";
@Rule
public EasyMockRule em = new EasyMockRule(this);
@Mock
private TokenService tokenService;
@Mock
private ApplicationRegistry applicationRegistry;
@Mock
private RequestLimitingService requestLimitingService;
@TestSubject
private AuthorizationService testedObject = new AuthorizationService();
@Test
public void testAuthorizeOk() {
User u = new User();
expect(tokenService.validateToken(TOKEN)).andStubReturn(u);
expect(tokenService.getApplicationId(TOKEN)).andStubReturn(APP_ID);
expect(applicationRegistry.validateApplicationId(APP_ID)).andStubReturn(true);
expect(requestLimitingService.checkLimit(u, APP_ID)).andStubReturn(true);
replay(tokenService, applicationRegistry, requestLimitingService);
assertTrue(testedObject.authorize(TOKEN));
}
@Test
public void testAuthorizeExtendedOk() {
User u = new User();
expect(tokenService.validateToken(TOKEN)).andStubReturn(u);
expect(tokenService.getApplicationId(TOKEN)).andStubReturn(APP_ID);
expect(applicationRegistry.validateApplicationId(APP_ID)).andStubReturn(true);
expect(requestLimitingService.checkLimit(u, APP_ID)).andStubReturn(false);
expect(requestLimitingService.checkExtendedLimit(u)).andStubReturn(true);
replay(tokenService, applicationRegistry, requestLimitingService);
assertTrue(testedObject.authorize(TOKEN));
}
//And so long....
}
|
Тест с параметрами
А ведь отличаются тесты то только параметрами! Попробуем отрефакторить тест так, чтобы в нём был ровно один тестовый метод. Определим возможные варианты исполнения:
1
2
3
4
5
6
7
8
9
10
11
|
@Parameterized.Parameters(name = "{index}: Token: {1}, User: {2}, AppID: {3}, appValidity: {4}, noOveruser: {5}, noExtendedOveruse: {6}, Result: {7}")
public static Iterable<Object[]> data() {
return Arrays.asList(new Object[][]{
{TOKEN, USER, APP_ID, true, true, null, true}, //Happy path
{TOKEN, USER, APP_ID, true, false, true, true}, //Extended overuse
{TOKEN, null, null, null, null, null, false}, //Token not valid
{TOKEN, USER, null, null, null, null, false}, //No app id
{TOKEN, USER, APP_ID, false, null, null, false}, //App id is invalid
{TOKEN, USER, APP_ID, true, false, false, false} //Resources overused
});
}
|
Подтоговим класс теста к использованию параметров:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
@RunWith(Parameterized.class)
public class ParameterizedAuthorizationServiceTest {
//Test data skipped
private String token;
private User u;
private String appId;
private Boolean appValidity;
private Boolean overuse;
private Boolean extendedOveruse;
private Boolean result;
public ParameterizedAuthorizationServiceTest(String token, User u, String appId, Boolean appValidity, Boolean overuse, Boolean extendedOveruse, Boolean result) {
this.token = token;
this.u = u;
this.appId = appId;
this.appValidity = appValidity;
this.overuse = overuse;
this.extendedOveruse = extendedOveruse;
this.result = result;
}
|
И перепишем сам тест:
1
2
3
4
5
6
7
8
9
10
11
12
|
@Test
public void testAuthorize() {
expect(tokenService.validateToken(token)).andStubReturn(u);
expect(tokenService.getApplicationId(token)).andStubReturn(appId);
expect(applicationRegistry.validateApplicationId(appId)).andStubReturn(appValidity);
expect(requestLimitingService.checkLimit(u, appId)).andStubReturn(overuse);
expect(requestLimitingService.checkExtendedLimit(u)).andStubReturn(extendedOveruse);
replay(tokenService, applicationRegistry, requestLimitingService);
assertThat(testedObject.authorize(token), is(result));
}
|
Класс теста почти не изменился, но зато теперь у нас один тест покрывает все возможные варианты исполнения. Или почти все — мы забыли про кидающий NPE RequestLimitingService Что ж, костыль в коде — костыль в тесте. Немного подправим параметры:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
@Parameterized.Parameters(name = "{index}: Token: {1}, User: {2}, AppID: {3}, appValidity: {4}, noOveruser: {5}, noExtendedOveruse: {6}, Result: {7}")
public static Iterable<Object[]> data() {
return Arrays.asList(new Object[][]{
{TOKEN, USER, APP_ID, true, true, null, true}, //Happy path
{TOKEN, USER, APP_ID, true, false, true, true}, //Extended overuse
{TOKEN, null, null, null, null, null, false}, //Token not valid
{TOKEN, USER, null, null, null, null, false}, //No app id
{TOKEN, USER, APP_ID, false, null, null, false}, //App id is invalid
{TOKEN, USER, APP_ID, true, false, false, false}, //Resources overused
{TOKEN, USER, APP_ID, true, null, null, false}, //NPE in limiting service
{TOKEN, USER, APP_ID, true, false, null, false} //NPE in limiting service
});
}
|
Я добавил две строки, исполнение которых должно привести к возврату из методов RequestLimitingService null. А в тесте я сделаю подпорку под этот null:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
@Test
public void testAuthorize() {
expect(tokenService.validateToken(token)).andStubReturn(u);
expect(tokenService.getApplicationId(token)).andStubReturn(appId);
expect(applicationRegistry.validateApplicationId(appId)).andStubReturn(appValidity);
if (overuse != null) {
expect(requestLimitingService.checkLimit(u, appId)).andStubReturn(overuse);
} else {
expect(requestLimitingService.checkLimit(u, appId)).andThrow(new NullPointerException());
}
if (extendedOveruse != null) {
expect(requestLimitingService.checkExtendedLimit(u)).andStubReturn(extendedOveruse);
} else {
expect(requestLimitingService.checkExtendedLimit(u)).andThrow(new NullPointerException());
}
replay(tokenService, applicationRegistry, requestLimitingService);
assertThat(testedObject.authorize(token), is(result));
}
|
Некрасиво конечно, ну да ладно 🙂 Проверим результаты:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running ru.easyjava.easymock.AuthorizationServiceTest
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.004 sec - in ru.easyjava.easymock.AuthorizationServiceTest
Running ru.easyjava.easymock.ParameterizedAuthorizationServiceTest
рту 17, 2015 4:15:45 PM ru.easyjava.easymock.service.AuthorizationService authorize
WARNING: limiting service failed
рту 17, 2015 4:15:45 PM ru.easyjava.easymock.service.AuthorizationService authorize
WARNING: limiting service failed
Tests run: 8, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.006 sec - in ru.easyjava.easymock.ParameterizedAuthorizationServiceTest
Results :
Tests run: 10, Failures: 0, Errors: 0, Skipped: 0
[INFO] ------------------------------------------------------------------------
|
Все тесты проходят 🙂
В чём же профит? Профитов несколько: класс теста не представляет собой нереально длинную портянку почти одинаковых методов, в которых можно запутаться; при изменении тестируемого кода достаточно поменять ровно один тест; из параметров сразу видно, какие значения к какому результату приводят.
Код примера доступен на github.