Статьи

Студент проекта: данные тестов интеграции Sharding

Это часть проекта Студент . Другие посты: Клиент Webservice с Джерси , Сервер Webservice с Джерси , Бизнес-уровень и Постоянство с Spring Data .

Все интеграционные тесты до сих пор использовали встроенную базу данных в памяти, которая не сохраняла информацию от запуска к запуску. Это меняется, когда мы полностью интегрируем сервер REST с «реальным» сервером базы данных — оставшиеся тестовые данные будут загрязнять нашу базу данных разработки или тестирования. Это может быть настоящей головной болью, если у нас есть постоянная интеграция, которая запускает тесты интеграции.

Одним из решений является «ограждение» данных наших интеграционных тестов таким образом, чтобы наши тесты могли использовать общую базу данных разработки, не загрязняя ее или другие тесты. Самый простой подход — добавить поле TestRun ко всем нашим объектам. «Тестовые» данные будут иметь значение, которое указывает на конкретный тестовый запуск, «живые» данные будут иметь нулевое значение.

Точный график времени

  1. создать и сохранить объект TestRun
  2. создавать тестовые объекты с соответствующим значением TestRun
  3. выполнить интеграционные тесты
  4. удалить тестовые объекты
  5. удалить объект TestRun

Любая запись в таблице TestRun будет: 1) активными интеграционными тестами или 2) неудачными интеграционными тестами, которые вызвали необработанное исключение (конечно, в зависимости от менеджера транзакций). Важно отметить, что мы также можем фиксировать состояние базы данных после того, как возникло непредвиденное исключение, даже если менеджер транзакций выполняет откат — это простое расширение для запуска тестов junit.

Временная метка и пользовательские поля позволяют легко удалить устаревшие данные теста в соответствии с его возрастом (например, любой тест старше 7 дней) или человеком, который выполнял тест.

TestablePersistentObject абстрактный базовый класс

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

Сначала мы расширяем наш абстрактный базовый класс PersistentObject значением тестового прогона.

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
@MappedSuperclass
public abstract class TestablePersistentObject extends PersistentObject {
    private static final long serialVersionUID = 1L;
    private TestRun testRun;
 
    /**
     * Fetch testRun object. We use lazy fetching since we rarely care about the
     * contents of this object - we just want to ensure referential integrity to
     * an existing testRun object when persisting a TPO.
     *
     * @return
     */
    @ManyToOne(fetch = FetchType.LAZY, optional = true)
    public TestRun getTestRun() {
        return testRun;
    }
 
    public void setTestRun(TestRun testRun) {
        this.testRun = testRun;
    }
 
    @Transient
    public boolean isTestData() {
        return testRun != null;
    }
}

TestRun класс

Класс TestRun содержит идентификационную информацию об одном прогоне интеграционного теста. Он содержит имя (по умолчанию имя класса # methodname () окружающего интеграционного теста), дату и время теста, а также имя пользователя, выполняющего тест. Было бы легко собрать дополнительную информацию.

Список тестовых объектов дает нам две большие победы. Во-первых, это позволяет легко фиксировать состояние базы данных при необходимости (например, после непредвиденного исключения). Во-вторых, каскадное удаление позволяет легко удалить все тестовые объекты.

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
72
73
74
75
76
@XmlRootElement
@Entity
@Table(name = "test_run")
@AttributeOverride(name = "id", column = @Column(name = "test_run_pkey"))
public class TestRun extends PersistentObject {
    private static final long serialVersionUID = 1L;
 
    private String name;
    private Date testDate;
    private String user;
    private List<TestablePersistentObject> objects = Collections.emptyList();
 
    @Column(length = 80, unique = false, updatable = true)
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    @Column(name = "test_date", nullable = false, updatable = false)
    @Temporal(TemporalType.TIMESTAMP)
    public Date getTestDate() {
        return testDate;
    }
 
    public void setTestDate(Date testDate) {
        this.testDate = testDate;
    }
 
    @Column(length = 40, unique = false, updatable = false)
    public String getUser() {
        return user;
    }
 
    public void setUser(String user) {
        this.user = user;
    }
 
    @OneToMany(cascade = CascadeType.ALL)
    public List<TestablePersistentObject> getObjects() {
        return objects;
    }
 
    public void setObjects(List<TestablePersistentObject> objects) {
        this.objects = objects;
    }
 
    /**
     * This is similar to standard prepersist method but we also set default
     * values for everything else.
     */
    @PrePersist
    public void prepersist() {
        if (getCreationDate() == null) {
            setCreationDate(new Date());
        }
 
        if (getTestDate() == null) {
            setTestDate(new Date());
        }
 
        if (getUuid() == null) {
            setUuid(UUID.randomUUID().toString());
        }
 
        if (getUser() == null) {
            setUser(System.getProperty("user.name"));
        }
 
        if (name == null) {
            setName("test run " + getUuid());
        }
    }
}

Класс TestRun расширяет PersistentObject, а не TestablePersistentObject, поскольку наши другие интеграционные тесты будут в достаточной степени его использовать.

Spring Data Repository

Мы должны добавить один дополнительный метод в каждый репозиторий.

1
2
3
4
5
6
@Repository
public interface CourseRepository extends JpaRepository {
    List<Course> findCoursesByTestRun(TestRun testRun);
 
    ....
}

Сервисный интерфейс

Аналогичным образом мы должны добавить два дополнительных метода для каждого сервиса.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
public interface CourseService {
    List<Course> findAllCourses();
 
    Course findCourseById(Integer id);
 
    Course findCourseByUuid(String uuid);
 
    Course createCourse(String name);
 
    Course updateCourse(Course course, String name);
 
    void deleteCourse(String uuid);
 
    // new method for testing
    Course createCourseForTesting(String name, TestRun testRun);
 
    // new method for testing
    List<Course> findAllCoursesForTestRun(TestRun testRun);
}

Я не буду показывать TestRunRepository, интерфейс TestRunService или реализацию TestRunService, поскольку они идентичны тем, что я описал в последних нескольких записях блога.

Внедрение сервиса

Мы должны внести одно небольшое изменение в существующую реализацию Сервиса плюс добавить два новых метода.

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
72
@Service
public class CourseServiceImpl implements CourseService {
    @Resource
    private TestRunService testRunService;
 
    /**
     * @see com.invariantproperties.sandbox.student.business.CourseService#
     *      findAllCourses()
     */
    @Transactional(readOnly = true)
    @Override
    public List<Course> findAllCourses() {
        List<Course> courses = null;
 
        try {
            courses = courseRepository.findCoursesByTestRun(null);
        } catch (DataAccessException e) {
            if (!(e instanceof UnitTestException)) {
                log.info("error loading list of courses: " + e.getMessage(), e);
            }
            throw new PersistenceException("unable to get list of courses.", e);
        }
 
        return courses;
    }
 
    /**
     * @see com.invariantproperties.sandbox.student.business.CourseService#
     *      findAllCoursesForTestRun(com.invariantproperties.sandbox.student.common.TestRun)
     */
    @Transactional(readOnly = true)
    @Override
    public List<Course> findAllCoursesForTestRun(TestRun testRun) {
        List<Course> courses = null;
 
        try {
            courses = courseRepository.findCoursesByTestRun(testRun);
        } catch (DataAccessException e) {
            if (!(e instanceof UnitTestException)) {
                log.info("error loading list of courses: " + e.getMessage(), e);
            }
            throw new PersistenceException("unable to get list of courses.", e);
        }
 
        return courses;
    }
 
    /**
     * @see com.invariantproperties.sandbox.student.business.CourseService#
     *      createCourseForTesting(java.lang.String,
     *      com.invariantproperties.sandbox.student.common.TestRun)
     */
    @Transactional
    @Override
    public Course createCourseForTesting(String name, TestRun testRun) {
        final Course course = new Course();
        course.setName(name);
        course.setTestUuid(testRun.getTestUuid());
 
        Course actual = null;
        try {
            actual = courseRepository.saveAndFlush(course);
        } catch (DataAccessException e) {
            if (!(e instanceof UnitTestException)) {
                log.info("internal error retrieving course: " + name, e);
            }
            throw new PersistenceException("unable to create course", e);
        }
 
        return actual;
    }
}

CourseServiceIntegrationTest

Мы вносим несколько изменений в наши интеграционные тесты. Нам нужно изменить только один метод тестирования, поскольку он единственный, который фактически создает тестовый объект. Остальные методы — это запросы, которые не требуют тестовых данных.

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

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
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { BusinessApplicationContext.class, TestBusinessApplicationContext.class,
        TestPersistenceJpaConfig.class })
@Transactional
@TransactionConfiguration(defaultRollback = true)
public class CourseServiceIntegrationTest {
 
    @Resource
    private CourseService dao;
 
    @Resource
    private TestRunService testService;
 
    @Test
    public void testCourseLifecycle() throws Exception {
        final TestRun testRun = testService.createTestRun();
 
        final String name = "Calculus 101 : " + testRun.getUuid();
 
        final Course expected = new Course();
        expected.setName(name);
 
        assertNull(expected.getId());
 
        // create course
        Course actual = dao.createCourseForTesting(name, testRun);
        expected.setId(actual.getId());
        expected.setUuid(actual.getUuid());
        expected.setCreationDate(actual.getCreationDate());
 
        assertThat(expected, equalTo(actual));
        assertNotNull(actual.getUuid());
        assertNotNull(actual.getCreationDate());
 
        // get course by id
        actual = dao.findCourseById(expected.getId());
        assertThat(expected, equalTo(actual));
 
        // get course by uuid
        actual = dao.findCourseByUuid(expected.getUuid());
        assertThat(expected, equalTo(actual));
 
        // get all courses
        final List<Course> courses = dao.findCoursesByTestRun(testRun);
        assertTrue(courses.contains(actual));
 
        // update course
        expected.setName("Calculus 102 : " + testRun.getUuid());
        actual = dao.updateCourse(actual, expected.getName());
        assertThat(expected, equalTo(actual));
 
        // verify testRun.getObjects
        final List<TestablePersistentObject> objects = testRun.getObjects();
        assertTrue(objects.contains(actual));
 
        // delete Course
        dao.deleteCourse(expected.getUuid());
        try {
            dao.findCourseByUuid(expected.getUuid());
            fail("exception expected");
        } catch (ObjectNotFoundException e) {
            // expected
        }
 
        testService.deleteTestRun(testRun.getUuid());
    }
 
    ....
}

Мы могли бы использовать @Before и @After для прозрачной упаковки всех методов тестирования, но многие тесты не требуют тестовых данных, а многие тесты, которые требуют тестовых данных, требуют уникальных тестовых данных, например, для адресов электронной почты. В последнем случае мы складываем тестовый UUID, как указано выше.

REST Webservice Server

Веб-сервис REST требует добавления тестового uuid к классам запросов и добавления некоторой логики для правильной обработки при создании объекта.

Веб-сервис REST не поддерживает получение списка всех тестовых объектов. «Правильный» подход заключается в создании службы TestRun и предоставлении связанных объектов в ответ на запрос / get / {id}.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
@XmlRootElement
public class Name {
    private String name;
    private String testUuid;
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public String getTestUuid() {
        return testUuid;
    }
 
    public void setTestUuid(String testUuid) {
        this.testUuid = testUuid;
    }
}

Теперь мы можем проверить наличие необязательного поля testUuid и вызвать соответствующий метод create.

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
@Service
@Path("/course")
public class CourseResource extends AbstractResource {
    @Resource
    private CourseService service;
 
    @Resource
    private TestRunService testRunService;
 
    /**
     * Create a Course.
     *
     * @param req
     * @return
     */
    @POST
    @Consumes({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })
    @Produces({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })
    public Response createCourse(Name req) {
        log.debug("CourseResource: createCourse()");
 
        final String name = req.getName();
        if ((name == null) || name.isEmpty()) {
            return Response.status(Status.BAD_REQUEST).entity("'name' is required'").build();
        }
 
        Response response = null;
 
        try {
            Course course = null;
 
            if (req.getTestUuid() != null) {
                TestRun testRun = testRunService.findTestRunByUuid(req.getTestUuid());
                if (testRun != null) {
                    course = service.createCourseForTesting(name, testRun);
                } else {
                    response = Response.status(Status.BAD_REQUEST).entity("unknown test UUID").build();
                }
            } else {
                course = service.createCourse(name);
            }
            if (course == null) {
                response = Response.status(Status.INTERNAL_SERVER_ERROR).build();
            } else {
                response = Response.created(URI.create(course.getUuid())).entity(scrubCourse(course)).build();
            }
        } catch (Exception e) {
            if (!(e instanceof UnitTestException)) {
                log.info("unhandled exception", e);
            }
            response = Response.status(Status.INTERNAL_SERVER_ERROR).build();
        }
 
        return response;
    }
 
    ....
}

REST Webservice Client

Наконец, REST-сервер должен добавить еще один метод. Клиент пока не поддерживает получение списка всех тестовых объектов.

01
02
03
04
05
06
07
08
09
10
11
12
public interface CourseRestClient {
 
    /**
     * Create specific course for testing.
     *
     * @param name
     * @param testRun
     */
    Course createCourseForTesting(String name, TestRun testRun);
 
    ....
}

и

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
public class CourseRestClientImpl extends AbstractRestClientImpl implements CourseRestClient {
 
    /**
     * Create JSON string.
     *
     * @param name
     * @return
     */
    String createJson(final String name, final TestRun testRun) {
        return String.format("{ \"name\": \"%s\", \"testUuid\": \"%s\" }", name, testRun.getTestUuid());
    }
 
    /**
     * @see com.invariantproperties.sandbox.student.webservice.client.CourseRestClient#createCourse(java.lang.String)
     */
    @Override
    public Course createCourseForTesting(final String name, final TestRun testRun) {
        if (name == null || name.isEmpty()) {
            throw new IllegalArgumentException("'name' is required");
        }
 
        if (testRun == null || testRun.getTestUuid() == null || testRun.getTestUuid().isEmpty()) {
            throw new IllegalArgumentException("'testRun' is required");
        }
 
        return createObject(createJson(name, testRun));
    }
 
    ....
}

Исходный код

Исходный код доступен по адресу http://code.google.com/p/invariant-properties-blog/source/browse/student .

осветление

Я не думал, что было возможно иметь @OneToMany для TestablePersistentObject в TestRun, но интеграционные тесты с использованием H2 были успешными. К сожалению, это вызывает проблемы, так как я использую полностью интегрированный веб-сервис с базой данных PostgreSQL. Я оставляю код на месте выше, так как всегда есть список Классных комнат, список Курсов и т. Д., Даже если у нас нет общей коллекции. Однако код удаляется из версии под контролем исходного кода.

коррекция

Метод интерфейса должен быть findCourseByTestRun_Uuid () , а не findCourseByTestRun () . Другой подход заключается в использовании запросов критериев JPA — см. Project Student: JPA Criteria Queries .