Внедрение макета репозитория в службу не вводит правильный макет метода (Spring, JUnit и Mockito) - PullRequest
0 голосов
/ 04 августа 2020

tl; dr : Похоже на Mock репозитория, который я создал с настраиваемым поведением в отношении метода save при введении теряет настраиваемое поведение.

Описание проблемы

Я пытался протестировать службу весной. В частности, интересующий метод принимает некоторые параметры и создает User , который сохраняется в UserRepository с помощью метода репозитория save .

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

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

Я основал на себе этот вопрос , чтобы сохранить User .

private static User savedUser;

public UserRepository createMockRepo() {
   UserRepository mockRepo = mock(UserRepository.class);
   try {
      doAnswer(new Answer<Void>() {
            @Override
            public Void answer(InvocationOnMock invocation) throws Throwable {
                savedUser= (User) invocation.getArguments(0);
                return null;
            }
        }).when(mockRepo).save(any(User.class));
   } catch( Exception e) {}
   return mockRepo;
}

private UserRepository repo = createMockRepo();

Два примечания:

  • Я дал имя репо в случае, если имя было

  • Нет аннотации @ Mock , так как она начинает проваливать тест, я предполагаю, что это потому, что она создаст макет в обычным способом (без специального метода, который я создал ранее ).

Затем я создал тестовую функцию, чтобы проверить, имеет ли она желаемое поведение и все ли в порядке.

@Test 
void testRepo() {
   User u = new User();
   repo.save(u);
   assertSame(u, savedUser);
}

Затем я попытался сделать то, что я рекомендовал через несколько вопросов, то есть , чтобы ввести макет в службу , как описано здесь .

@InjectMocks
private UserService service = new UserService();

@Before
public void setup() {
   MockitoAnnotations.initMocks(this);
}

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

@Test 
void testUser() {
   String name = "Steve";
   String food = "Apple";
   
   service.newUser(name, food);

   assertEquals(savedUser.getName(), name);
   assertEquals(savedUser.getFood(), food);
}

После отладки:

Я решил зарегистрировать функцию с помощью System.out.println для демонстрационных целей.

Распечатка моего журнала тестов, демонстрирующая, что пользовательский тест выполняет Не вызывайте метод answer

Что я здесь делаю не так?

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

Ответы [ 2 ]

1 голос
/ 04 августа 2020

Вместо того, чтобы создавать экземпляр вашей службы в тестовом классе, как это делали вы, используйте @Autowired и убедитесь, что в вашем UserRepository есть @MockBean в тестовом классе

@InjectMocks
@Autowired
private UserService service

@MockBean
private UserRepository mockUserRepo

Таким образом, вы можете удалить свой метод настройки

Но убедитесь, что ваш репозиторий пользователей также автоматически подключен к вашей службе

0 голосов
/ 05 августа 2020

Вам не понадобится Spring, чтобы это проверить. Если вы следуете лучшим практикам Spring, когда дело доходит до зависимостей автоматического подключения, вы должны иметь возможность просто создавать объекты самостоятельно и передавать UserRepository в UserService

Лучшие практики:

  • Внедрение конструктора для требуемых bean-компонентов
  • Внедрение установщика для необязательных bean-компонентов
  • Внедрение поля никогда, кроме случаев, когда вы не можете внедрить конструктор или установщик, что очень и очень редко.

Обратите внимание, что InjectMocks не является фреймворком для внедрения зависимостей, и я не рекомендую его использовать. Вы можете увидеть в javado c, что он может стать довольно сложным, когда дело доходит до конструктора против установщика против поля.

Обратите внимание, что здесь можно найти рабочие примеры кода в этом репозитории GitHub.

Простой способ очистить ваш код и сделать его более легким для тестирования - это исправить UserService, чтобы вы могли пройти любую реализацию UserRepository вы хотите, это также позволяет вам гарантировать неизменяемость,

public class UserService {

  public UserService(final UserRepository userRepository) {
    this.userRepository = userRepository;
  }

  public final UserRepository userRepository;

  public User newUser(String name, String food) {
    var user = new User();
    user.setName(name);
    user.setFood(food);
    return userRepository.save(user);
  }
}

, и тогда ваш тест станет более простым,

class UserServiceTest {

  private UserService userService;
  private UserRepository userRepository;

  private static User savedUser;

  @BeforeEach
  void setup() {
    userRepository = createMockRepo();
    userService = new UserService(userRepository);
  }

  @Test
  void testSaveUser(){
    String name = "Steve";
    String food = "Apple";

    userService.newUser(name, food);

    assertEquals(savedUser.getName(), name);
    assertEquals(savedUser.getFood(), food);
  }

  public UserRepository createMockRepo() {
    UserRepository mockRepo = mock(UserRepository.class);
    try {
      doAnswer(
              (Answer<Void>) invocation -> {
                savedUser = (User) invocation.getArguments()[0];
                return null;
              })
          .when(mockRepo)
          .save(any(User.class));
    } catch (Exception e) {
    }
    return mockRepo;
  }
}

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

  • Как работают аннотации @Id?
  • А как насчет Hibernate JPA взаимодействует с моим Entitiy?
  • Соответствуют ли определения столбцов в моем Entitiy тому, что я бы сделал eploy против при использовании чего-то вроде Liquibase / Flyway для управления миграциями базы данных?
  • Как проверить наличие любых ограничений, которые может иметь база данных?
  • Как проверить настраиваемые границы транзакций?

Вы запекаете множество предположений, для этого вы можете использовать аннотацию @ DataJpaTest документации, которую предоставляет Spring Boot, или реплицировать конфигурацию. На этом этапе я предполагаю, что это приложение Spring Boot, но та же концепция применима к приложениям Spring Framework, вам просто нужно настроить конфигурации et c.

@DataJpaTest
class BetterUserServiceTest {

  private UserService userService;

  @BeforeEach
  void setup(@Autowired UserRepository userRepository) {
    userService = new UserService(userRepository);
  }

  @Test
  void saveUser() {
    String name = "Steve";
    String food = "Apple";

    User savedUser = userService.newUser(name, food);

    assertEquals(savedUser.getName(), name);
    assertEquals(savedUser.getFood(), food);
  }
}

В этом примере мы пошли дальше и удалили любое понятие имитации, подключаемся к базе данных в памяти и проверяем, что возвращенный пользователь не изменился на то, что мы сохранили .

Тем не менее, существуют ограничения для баз данных в памяти для тестирования, поскольку мы обычно развертываем что-то вроде MySQL, DB2, Postgres et c. где определения столбцов (например) не могут быть точно воссозданы базой данных в памяти для каждой «реальной» базы данных.

Мы могли бы пойти дальше и использовать Testcontainers, чтобы развернуть docker изображение база данных, к которой мы будем подключаться во время выполнения и подключаться к ней в рамках теста

@DataJpaTest
@Testcontainers(disabledWithoutDocker = true)
class BestUserServiceTest {

  private UserService userService;

  @BeforeEach
  void setup(@Autowired UserRepository userRepository) {
    userService = new UserService(userRepository);
  }

  @Container private static final MySQLContainer<?> MY_SQL_CONTAINER = new MySQLContainer<>();

  @DynamicPropertySource
  static void setMySqlProperties(DynamicPropertyRegistry properties) {
    properties.add("spring.datasource.username", MY_SQL_CONTAINER::getUsername);
    properties.add("spring.datasource.password", MY_SQL_CONTAINER::getPassword);
    properties.add("spring.datasource.url", MY_SQL_CONTAINER::getJdbcUrl);
  }

  @Test
  void saveUser() {
    String name = "Steve";
    String food = "Apple";

    User savedUser = userService.newUser(name, food);

    assertEquals(savedUser.getName(), name);
    assertEquals(savedUser.getFood(), food);
  }
}

Теперь мы точно тестируем, можем ли мы сохранить и сопоставить нашего пользователя с реальной базой данных MySQL. Если мы сделаем еще один шаг и добавим журналы изменений и т. Д. c. они также могут быть зафиксированы в этих тестах.

...