Статьи

Советы по (модульному тестированию) JavaBeans

Если вы пишете код Java, скорее всего, вы пишете, по крайней мере, несколько классов, которые соответствуют соглашениям JavaBean, то есть классы, которые имеют частные свойства с общедоступными методами getter и setter, содержат конструктор без аргументов, сериализуются и соблюдать контракты Equals и HashCode. Кроме того, вы, вероятно, также добавите полезную реализацию toString ().

Если, например, мы возьмем очень простой класс MyBean, который содержит два поля с именами id и name, мы получим следующий код:

MyBean — пример JavaBean

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
package it.jdev.example;
 
import java.io.Serializable;
 
public class MyBean implements Serializable {
 
    private static final long serialVersionUID = 6170536066049208199L;
 
    private long id;
    private String name;
 
    public MyBean() {
        super();
    }
 
    public long getId() {
        return id;
    }
 
    public void setId(final long id) {
        this.id = id;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(final String name) {
        this.name = name;
    }
 
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + (int) (id ^ (id >>> 32));
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        return result;
    }
 
    @Override
    public boolean equals(final Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final MyBean other = (MyBean) obj;
        if (id != other.id) {
            return false;
        }
        if (name == null) {
            if (other.name != null) {
                return false;
            }
        } else if (!name.equals(other.name)) {
            return false;
        }
        return true;
    }
 
    @Override
    public String toString() {
        return "MyBean [id=" + id + ", name=" + name + "]";
    }
 
}

Итак, для класса с двумя полями мы получили 70 строк кода. Это много шаблонного кода. И более того, каждый раз, когда вы добавляете или изменяете какие-либо свойства, вам придется настраивать или восстанавливать много этого стандартного кода.

Проект Ломбок на помощь

К счастью, есть хороший инструмент с открытым исходным кодом, целью которого является сокращение только того типового кода, с которым мы имеем дело в нашем классе MyBean. Это называется Project Lombok . Просто установите Lombok как плагин в вашей любимой IDE и включите файл Jar Lombok в путь к вашей сборке или добавьте его как зависимость maven, и все будет хорошо.

Project Lombok содержит множество различных аннотаций, но для нашего примера нам понадобится только одна: @Data. Когда мы применяем аннотацию к нашему коду, мы получаем всего 15 строк кода из наших исходных 70 строк, а Project Lombok генерирует все методы для нас во время компиляции. И более того, нам больше никогда не придется беспокоиться о том, что наши методы hashCode (), equals () и toString () работают вне синхронизации.

MyBean — наш пример JavaBean с Project Lombok

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
package it.jdev.example;
 
import java.io.Serializable;
 
import lombok.Data;
 
@Data
public class MyBean implements Serializable {
 
    private static final long serialVersionUID = 6170536066049208199L;
 
    private long id;
    private String name;
 
}

Помогите, мой код покрытия не работает

Тот факт, что теперь у нас есть Project Lombok, генерирующий шаблонный код для нас, не обязательно означает, что мы можем пропустить модульное тестирование сгенерированных методов. Особенно, если вы цените покрытие кода, и у вас есть минимальные проверки уровня покрытия в вашей настройке CI, вы захотите добавить несколько дополнительных тестов. К счастью, есть несколько простых способов увеличить охват кода.

Тестирование Сериализуемости

Если ваши сериализуемые объекты содержат какие-либо настраиваемые поля, они, вероятно, также должны быть сериализуемыми. Тем не менее, это то, что легко упустить из виду. Используя класс SerializationUtils из библиотеки Apache Commons Lang, вы можете написать очень простой тест, который проверяет, правильно ли сериализуется объект, и снова десериализуется.

Тестирование на сериализуемость нашего MyBean

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
package it.jdev.example;
 
import static org.junit.Assert.*;
 
import org.apache.commons.lang3.SerializationUtils;
import org.junit.Before;
import org.junit.Test;
 
public class MyBeanTest {
 
    private MyBean myBean;
 
    @Before
    public void setUp() throws Exception {
        myBean = new MyBean();
        myBean.setId(123L);
        myBean.setName("Bean, James Bean");
    }
 
    @Test
    public void beanIsSerializable() {
        final byte[] serializedMyBean = SerializationUtils.serialize(myBean);
        final MyBean deserializedMyBean = (MyBean) SerializationUtils.deserialize(serializedMyBean);
        assertEquals(myBean, deserializedMyBean);
    }
 
}

Тестирование методов получения и установки

Тестирование пар методов получения и установки JavaBean может стать очень утомительным и очень быстрым. К счастью, есть хорошая библиотека тестов под названием meanBean, которая может сделать эту работу за нас. Поэтому после добавления следующего метода в наш модульный тест мы закончили тестирование геттеров и сеттеров:

Тестирование геттеров и сеттеров нашего примера MyBean

1
2
3
4
@Test
public void getterAndSetterCorrectness() throws Exception {
    new BeanTester().testBean(MyBean.class);
}

Тестирование equals () и hashCode ()

Тестирование всех хитростей контракта equals и hashCode — очень утомительная задача. Опять же, есть несколько хороших инструментов, которые могут снять его с рук. Вышеупомянутая библиотека meanBean предлагает для этого функциональные возможности. Однако я нашел такой инструмент, как EqualsVerifier, более строгим в своих тестах, и он также предоставляет подробное объяснение любых ошибок. Итак, мы собираемся добавить следующий тестовый пример в наш пакет:

Тестирование контрактов Equals и HashCode нашего примера MyBean

1
2
3
4
@Test
public void equalsAndHashCodeContract() throws Exception {
    EqualsVerifier.forClass(MyBean.class).suppress(Warning.STRICT_INHERITANCE, Warning.NONFINAL_FIELDS).verify();
}

Обратите внимание, что мы здесь подавляем некоторые предупреждения. Для получения дополнительной информации о причинах см. Информацию EqualsVerifier об ошибках: http://www.jqno.nl/equalsverifier/errormessages/ .

Общий базовый класс для наших тестов JavaBean

Даже с такими инструментами, как meanBean и EqualsVerifier, выполняющими тяжелую работу, вы не хотите повторять один и тот же тестовый код снова и снова. Так что вы, вероятно, захотите поместить тесты в абстрактный базовый класс. Возможная реализация этого базового класса может выглядеть примерно так:

Абстрактный базовый класс для тестирования JavaBeans

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
44
45
46
47
48
49
50
51
52
package it.jdev.example;
 
import static org.junit.Assert.assertEquals;
import java.io.Serializable;
import java.time.LocalDateTime;
import nl.jqno.equalsverifier.EqualsVerifier;
import nl.jqno.equalsverifier.Warning;
import org.apache.commons.lang3.SerializationUtils;
import org.junit.Test;
import org.meanbean.lang.Factory;
import org.meanbean.test.BeanTester;
 
public abstract class AbstractJavaBeanTest {
 
    protected String[] propertiesToBeIgnored;
 
    @Test
    public void beanIsSerializable() throws Exception {
        final T myBean = getBeanInstance();
        final byte[] serializedMyBean = SerializationUtils.serialize((Serializable) myBean);
        @SuppressWarnings("unchecked")
        final T deserializedMyBean = (T) SerializationUtils.deserialize(serializedMyBean);
        assertEquals(myBean, deserializedMyBean);
    }
 
    @Test
    public void equalsAndHashCodeContract() {
        EqualsVerifier.forClass(getBeanInstance().getClass()).suppress(Warning.STRICT_INHERITANCE, Warning.NONFINAL_FIELDS).verify();
    }
 
    @Test
    public void getterAndSetterCorrectness() throws Exception {
        final BeanTester beanTester = new BeanTester();
        beanTester.getFactoryCollection().addFactory(LocalDateTime.class, new LocalDateTimeFactory());
        beanTester.testBean(getBeanInstance().getClass());
    }
 
    protected abstract T getBeanInstance();
 
        /**
        * Concrete Factory that creates a LocalDateTime.
        */
        class LocalDateTimeFactory implements Factory {
 
        @Override
        public LocalDateTime create() {
            return LocalDateTime.now();
        }
 
    }
 
}

Обратите внимание, что — просто для удовольствия — я добавил LocalDateTimeFactory, чтобы meanBean мог тестировать методы получения и установки любых атрибутов LocalDateTime, которые вы могли бы использовать в своем классе JavaBean.

Применяя абстрактный базовый класс к модульному тесту нашего примера MyBean, получающийся в результате модульный тест будет выглядеть примерно так:

Финальный юнит-тест для нашего MyBean

01
02
03
04
05
06
07
08
09
10
11
12
13
14
package it.jdev.example;
 
import static org.junit.Assert.*;
 
import org.junit.Test;
 
public class MyBeanTest extends AbstractJavaBeanTest<MyBean> {
 
    @Override
    protected MyBean getBeanInstance() {
        return new MyBean();
    }
 
}