Весенний загрузочный тест: тест проходит, но не должен (ложноположительный результат)

У меня есть проект Spring Boot с тестом, который не терпит неудачу (но должен).

Я делаю что-то не так или это проблема с Spring?

В качестве небольшого примера я создал небольшой проект с двумя сущностями (пользователь и категория) и одним классом контроллера с методом DELETE (https://github.com/sk8ter/demo).

Сущность категории имеет идентификатор сущности пользователя без опции cascade, поэтому при удалении пользователя с категорией произойдет сбой:

@Entity
@Table(name = "user")
public class User {

    @Id
    @GeneratedValue
    private long id;
    private String name;
    ...
}

@Entity
@Table(name = "category")
public class Category {

    @Id
    @GeneratedValue
    private long id;
    private String name;

    @ManyToOne
    @JoinColumn(name = "user_id", nullable = false)
    private User user;
    ...
}

Контроллер тоже довольно прост:

@RestController
@RequestMapping(value = "/users", produces = "application/json;charset=UTF-8")
public class UserCategory {

    @Autowired
    private UserRepository userRepository;

    @ResponseStatus(HttpStatus.OK)
    @RequestMapping(value = "/{id}", method = DELETE, consumes =   MediaType.ALL_VALUE)
    public void deleteCategory(@PathVariable Long id) {
        User user = userRepository.getOne(id);
        userRepository.delete(user);
    }
}

И, наконец, тест:

@Transactional
@WebAppConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = DemoApplication.class)
public class DemoApplicationTests {

    @Autowired
    protected WebApplicationContext context;

    protected MockMvc mockMvc;

    @Before
    public void setUp() {
        mockMvc = MockMvcBuilders.webAppContextSetup(this.context).build();
    }

    @Test
    public void testName() throws Exception {
        mockMvc.perform(delete("/users/1"))
                .andExpect(status().isOk());

//      EntityManagerFactory entityManagerFactory = context.getBean(EntityManagerFactory.class);
//      SessionFactory sessionFactory = entityManagerFactory.unwrap(SessionFactory.class);
//      sessionFactory.getCurrentSession().flush();
    }
}

Тест завершится ошибкой, если я удалю аннотацию @Transactional из DemoApplicationTests, но в этом случае изменения будут зафиксированы в БД.

Закомментированные 3 строчки в тесте тоже не помогают.


person Viktor Kostov    schedule 24.11.2015    source источник
comment
Вы уверены, что у вас есть категория, в которой внешний ключ указывает на вашего пользователя?   -  person Oskar Dajnowicz    schedule 25.11.2015
comment
Да как еще я мог различать категории определенного пользователя.   -  person Viktor Kostov    schedule 25.11.2015
comment
У вас должна быть таблица ManyToMany (category_id, user_id), в противном случае вам придется дублировать одни и те же категории для разных пользователей, но это не ключ, я спрашиваю, уверены ли вы, что ваша база данных уже создала сущность категории, которая назначена определенному пользователю ? потому что, если вы это сделаете, БД не позволит вам удалить пользователя без каскадной опции, установленной в таблице, в противном случае, если вы просто удаляете пользователя, которому не назначена категория, тогда тест пройдет, потому что вы не злоупотребляете правилом внешнего ключа   -  person Oskar Dajnowicz    schedule 25.11.2015
comment
Это хороший момент, но категории уникальны для каждого пользователя, и мне не нужно делиться категориями. Это похоже на приложение Todo, где каждый пользователь создает свои категории для заметок. В случае, если я удаляю пользователя, я хочу удалить связанные категории (каскад), но проблема в том, что тест не показал эту проблему (поскольку у меня нет опции каскада). Так его нашел другой тестер. Я предполагаю, что в будущем будет больше тестов, которые пройдут, но не пройдут в рабочей среде, потому что тест не сбрасывает изменения.   -  person Viktor Kostov    schedule 25.11.2015
comment
Hibernate/JPA не проверяет, назначен ли объект где-либо внешним ключом, он просто создает соответствующие запросы sql (без опции каскада, он просто не создает каскадные sql), затем база данных проверяет, не нарушаете ли вы некоторые правила, и если вы тогда он выдает исключение, которое поймано в вашем коде, какую БД вы используете? Можете ли вы описать, что происходит с БД, когда вы пытаетесь удалить пользователя?   -  person Oskar Dajnowicz    schedule 25.11.2015
comment
Я понимаю это и хочу, чтобы это исключение было в моем тесте, чтобы я мог исправить его перед развертыванием на сервере dev/prod. В чем смысл теста, если он не работает? MySQL для тестирования H2.   -  person Viktor Kostov    schedule 25.11.2015


Ответы (1)


Я не хотел аннотировать метод или класс с помощью @Rollback(false), так как хотел, чтобы все мои тесты были идемпотент. В случае, если я аннотирую @Rollback(false), H2 также не работает с ограничениями внешнего ключа.

Я нашел решение:

@Transactional
@WebAppConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = DemoApplication.class)
public class DemoApplicationTests {

    @Autowired
    protected WebApplicationContext context;

    protected MockMvc mockMvc;

    @PersistenceContext
    EntityManager em;

    @Before
    public void setUp() {
        mockMvc = MockMvcBuilders.webAppContextSetup(this.context).build();
    }

    @Test
    public void testName() throws Exception {
        mockMvc.perform(delete("/users/1"))
                .andExpect(status().isOk());

        em.flush();
    }
}

Ключевые линии:

@PersistenceContext
EntityManager em;
...
// Manual flush is required to avoid false positive in test
em.flush();

Таким образом, SessionFactory не работает из Документация Spring

// Manual flush is required to avoid false positive in test
sessionFactory.getCurrentSession().flush();
person Viktor Kostov    schedule 25.11.2015
comment
Решение выглядит нормально, но SessionFactory не работает, потому что вы используете JPA, который обертывает его в диспетчере сущностей, и вам следует переписать свой тест, потому что вы на самом деле проверяете, в порядке ли статус, и после этого вы делаете em.flush() который 200 вызывает исключение - person Oskar Dajnowicz; 25.11.2015
comment
Что вы предлагаете? Не могли бы вы привести пример? - person Viktor Kostov; 26.11.2015