Тестирование. Я много думал о тестировании в последнее время. Как часть обзоров кода, которые я сделал для различных проектов, я видел тысячи строк непроверенного кода. Это не просто случай статистики охвата тестами, указывающий на это, это скорее случай отсутствия каких-либо тестов в этих проектах. И две причины, по которым я продолжаю слышать об этом печальном положении вещей? «У нас нет времени», за которым быстро следует «Мы сделаем это, когда закончим код».
То, что я представляю здесь, не является панацеей для тестирования. Он охватывает модульное тестирование и, в частности, модульное тестирование интерфейсов. Интерфейсы хорошие вещи. Интерфейсы определяют контракты. Интерфейсы, независимо от того, сколько у них реализаций, могут быть протестированы легко и без особых усилий. Давайте посмотрим, как, используя эту структуру класса в качестве примера.
CustomerService — это наш интерфейс. Он имеет два метода, чтобы сохранить пример простым, и описан ниже. Обратите внимание на javadoc — это то место, где описан контракт.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
public interface CustomerService { /** * Retrieve the customer from somewhere. * @param userName the userName of the customer * @return a non-null Customer instance compliant with the userName * @throws CustomerNotFoundException if a customer with the given user name can not be found */ Customer get(String userName) throws CustomerNotFoundException; /** * Persist the customer. * @param customer the customer to persist * @return the customer as it now exists in its persisted form * @throws DuplicateCustomerException if a customer with the user name already exists */ Customer create(Customer customer) throws DuplicateCustomerException; } |
Как видно из диаграммы, у нас есть две реализации этого класса, RemoteCustomerService и CachingCustomerService. Их реализации не показаны, потому что они не имеют значения. Как я могу это сказать? Все просто — мы тестируем контракт. Мы пишем тесты для каждого метода в интерфейсе вместе с каждой перестановкой контракта. Например, для get () нам нужно проверить, что происходит, когда присутствует клиент с данным именем пользователя, и что происходит, когда его нет.
01
02
03
04
05
06
07
08
09
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
|
public abstract class CustomerServiceTest { @Test public void testCreate() { CustomerService customerService = getCustomerService(); Customer customer = customerService.create( new Customer( "userNameA" )); Assert.assertNotNull(customer); Assert.assertEquals( "userNameA" , customer.getUserName()); } @Test (expected = DuplicateCustomerException. class ) public void testCreate_duplicate() { CustomerService customerService = getCustomerService(); Customer customer = new Customer( "userNameA" ); customerService.create(customer); customerService.create(customer); } @Test public void testGet() { CustomerService customerService = getCustomerService(); customerService.create( new Customer( "userNameA" )); Customer customer = customerService.get( "userNameA" ); Assert.assertNotNull(customer); Assert.assertEquals( "userNameA" , result.getUserName()); } @Test (expected = CustomerNotFoundException. class ) public void testGet_noUser() { CustomerService customerService = getCustomerService(); customerService.get( "userNameA" ); } public abstract CustomerService getCustomerService(); } |
Теперь у нас есть тест для контракта, и мы ни разу не упомянули ни одну из реализаций. Это означает две вещи:
- Нам не нужно дублировать тесты для каждой реализации. Это очень хорошая вещь.
- Ни одна из реализаций не тестируется. Мы можем исправить это, добавив один тестовый класс на реализацию. Поскольку каждый тестовый класс будет практически идентичен, я просто покажу тест RemoteCustomerService.
1
2
3
4
5
6
7
|
public class RemoteCustomerServiceTest extends CustomerServiceTest { public CustomerService getCustomerService() { return new RemoteCustomerService(); } } |
Вот и все! Теперь у нас есть очень простой способ протестировать несколько реализаций любого интерфейса, включив тяжелую работу заранее и сократив усилия по тестированию новых реализаций до одного простого метода.