Как вытащить данные внутри Спринг Теста?
У меня есть сервис, в котором используется в ДТО, хочу написать под него тест, но при попытки вытащить данные получаю NULL_POINTER_EXCEPTION
Метод внутри сервиса который хочу протестировать
@Transactional
public Long addStudent(CreateStudentDto dto){
Account account = new Account();
if (isAccountExist(dto.getEmail())){
account = accountRepository.findAccountByEmail(dto.getEmail());
return studentRepository.findStudentByIdAccount(account).getId();
}
else {
String generatePassword = generatePassword();
String generateSalt = salt();
String hashPassword = hashString(generatePassword,generateSalt);
account.setRole((byte)1);
account.setPassword(hashPassword);
account.setSalt(generateSalt);
account.setIsActive(true);
if (Objects.nonNull(dto.getEmail())) account.setEmail(dto.getEmail());
accountRepository.save(account);
Student student = new Student();
student.setIdAccount(account);
studentRepository.save(student);
if (!StringUtils.isEmpty(account.getEmail())){
String message = "Your password - " + generatePassword;
mailSender.send(account.getEmail(),"Test Registration", message);
}
return student.getId();
}
}
Мой тест-класс
@RunWith(SpringRunner.class)
@SpringBootTest
public class StudentServiceTest {
@Autowired
private StudentService studentService;
@MockBean
private StudentRepository studentRepository;
@MockBean
private AccountRepository accountRepository;
@MockBean
private MailSender mailSender;
@Test
public void addStudent() {
CreateStudentDto dto = new CreateStudentDto();
dto.setEmail("dsds@gdd");
studentService.addStudent(dto);
Account account = accountRepository.findAccountByEmail(dto.getEmail());
when(accountRepository.findAccountByEmail(dto.getEmail())).thenReturn(account);
when(accountRepository.save(account)).thenReturn(account);
Assert.assertNotNull(dto.getEmail());
Assert.assertTrue(CoreMatchers.is(account.getRole()) // GET = NULL
.matches(Collections.singleton((byte)1)));
Assert.assertTrue(account.getIsActive()); // GET = NULL
Mockito.verify(mailSender, Mockito.times(1))
.send(ArgumentMatchers.eq(dto.getEmail()),
ArgumentMatchers.eq("Test Registration"),
ArgumentMatchers.contains("Your password - "));
}
При попытки получить роль аккаунта(account.getRole()), я получаю NULL, хотя внутри сервиса прописана роль, но данные не вытягиваются, что я делаю неправильно?
Добавил accountRepository
public interface AccountRepository extends CrudRepository<Account,Long> {
Account findAccountByEmail(String email);
}
Ответы (2 шт):
Сначала я объясню, как работают моки и как их использовать в тестах.
Когда в тесте, который запускается с помощью SpringRunner, какое-то поле помечено аннотацией @MockBean, то соответствующий бин в контексте спринга замещается моком. Это значит, что в контексте, который создается при запуске тесте, нет реальной реализации AccountRepository (т.е. той, которая сохраняет в базу данных или какое там сконфигурированное хранилище). Этот мок будет внедрен как зависимость в бины, которые используют AccountRepository (в нашем случае в StudentService).
Что же такое мок? Это объект, который реализует указанный интерфейс (для accountRepository это AccountRepository) и, каждый метод которого ничего не делает и по-умолчанию возвращает значение по умолчанию.
Рассмотрим на примере AccountRepository.
Если мок не конфигурировать, то вызов findAccountByEmail ничего сделает (в смысле не сохранит объект в БД) и вернет значение по умолчанию для Account, т.е. null (для всех объектных типов это будет null, а для примитивов типа int - 0, boolean - false и т.д. см таблицу).
Именно поэтому вызов:
Account account = accountRepository.findAccountByEmail(dto.getEmail());
в тесте возвращает null.
Тепер вкратце, зачем моки нужны.
Реализация класса StudentService зависит от других классов, в частности от AccountRepository. Для тестирования StudentService хочется:
- его изолировать от зависимостей, т.е. чтобы тестировать именно логику самого класса, а не логику еще и
AccountRepositoryи других - эмулировать ситуации, которые не всегда можно предсказуемо добиться в реальной системе (например, сбой сетевого подключения)
Для этого используются моки, т.е. "подделки". Суть в том, что перед выполнением тестового кода, мы их конфигурируем (или настраиваем), чтобы они делали, то что нам нужно для данного тестового сценария. Например, мы может указать, что при вызове findAccountByEmail должен вернуться null, так как account-а с такой почтой не существует.
Дополнительно после выполнения кода, который тестируется, мы можем проверить, что метод мока был вызван с параметрами, которые мы ожидаем в данном тестовом сценарии (например, что была установлена правильная роль).
Смотрим на пример из вопроса. Тут тест среди прочего должен проверить. что если не существует account-а с заданным почтовым адресом, то он будет создан и у него будет определенная роль.
Тест именно этого сценария будет выглядеть приблизительно так:
@Test
public void addStudentCreatesNewAccount() {
String someEmail = '[email protected]'
// тут я предполагаю, что isAccountExists пользуется findAccountByEmail
// чтобы определить существует ли account с заданой почтой
when(accountRepository.findAccountByEmail(someEmail)).thenReturn(null);
CreateStudentDto dto = new CreateStudentDto();
dto.setEmail(someEmail);
studentService.addStudent(dto);
// ArgumentCaptor позволяет захватить и запомнить значение, которое
// было передано, как аргумент в метод мока, чтобы его проверить
ArgumentCaptor<Account> accountCaptor = ArgumentCaptor.forClass(Account.class);
verify(accountRepository).save(accountCaptor.capture());
Account savedAccount = accountCaptor.getValue();
assertThat(savedAccount.getRole(), equalTo((byte)1));
assertThat(savedAccount.getEmail(), equalTo(someEmail));
}
Пример, только чтобы понять принцип. На практике, например, я ArgumentCaptor не использую, а пользуюсь MockitoHamcrest.argThat + матчеры. Далее, конфигурацию моков прячу в более высокоуровневые методы типа:
void givenAccountForEmailDoesNotExist(String email) {
when(accountRepository.findAccountByEmail(someEmail)).thenReturn(null);
}
и т.д. Главный принцип - спрятать детали реализации и сделать тест более читаемым и менее зависимым от деталей реализации.
Если я не ошибаюсь, @RunWith(XXXXX.class) это достаточно старая аннотация и можно не использовать. @SpringBootTest будет сверх достаточно в вашем классе.
Вместо CrudRepository используйте JpaRepository. Вам дополнительно будет доступно flush(), saveAndFlush(), getOne(). При тестировании, эти вещи очень помогают. Старайтесь так же не использовать @Transactional. Используйте его лишь там, где он реально нужен (к примеру при написании void deleteBy в файле репозитории)
Как я понимаю, @Transactional вы добавили из-за того, что у вас выползает ошибка в районе строки accountRepository.save(account); или return studentRepository.findStudentByIdAccount(account).getId();. Вот как я бы написал бы:
public Long addStudent(CreateStudentDto dto){
Account account = new Account();
if (isAccountExist(dto.getEmail())){
account = accountRepository.findAccountByEmail(dto.getEmail());
accountRepository.flush();
Student student = studentRepository.findStudentByIdAccount(account).getId()
return student;
}
else {
String generatePassword = generatePassword();
String generateSalt = salt();
String hashPassword = hashString(generatePassword,generateSalt);
account.setRole((byte)1);
account.setPassword(hashPassword);
account.setSalt(generateSalt);
account.setIsActive(true);
if (Objects.nonNull(dto.getEmail())) account.setEmail(dto.getEmail());
accountRepository.saveAndFlush(account); //добавил "saveAndFlush()"
Student student = new Student();
student.setIdAccount(account);
studentRepository.saveAndFlush(student);
if (!StringUtils.isEmpty(account.getEmail())){
String message = "Your password - " + generatePassword;
mailSender.send(account.getEmail(),"Test Registration", message);
}
return student.getId();
}
}
Так же меня интересует, почему от Student нельзя никак попасть в таблицу Account? Это абсолютно никак не связанные таблицы? Или между ними все-таки есть аннотация @OneToOne (как пример)?