Вприскування приватного поля @Autowired під час тестування


78

У мене є компонент налаштування, який по суті є панеллю запуску програми. Він налаштований так:

@Component
public class MyLauncher {
    @Autowired
    MyService myService;

    //other methods
}

MyService коментується @Serviceанотацією Spring і автоматично підключається до мого класу запуску без будь-яких проблем.

Я хотів би написати кілька тестових кейсів jUnit для MyLauncher, для цього я розпочав такий клас:

public class MyLauncherTest
    private MyLauncher myLauncher = new MyLauncher();

    @Test
    public void someTest() {

    }
}

Чи можу я створити об'єкт Mock для MyService і ввести його в myLauncher у своєму тестовому класі? В даний час у мене немає геттера чи сеттера в myLauncher, оскільки Spring обробляє автоматичне підключення. Якщо можливо, я б не хотів додавати геттери та сетери. Чи можу я сказати тестовому випадку, щоб вводити макетний об'єкт в автоматично підключену змінну за допомогою @Beforeметоду init?

Якщо я роблю це абсолютно неправильно, не соромтеся сказати це. Я все ще новачок у цьому. Моя головна мета - просто мати якийсь код Java або анотацію, яка поміщає макетний об’єкт у цю @Autowiredзмінну, без того, щоб мені доводилося писати метод сеттера чи використовувати applicationContext-test.xmlфайл. Я б набагато хотів зберегти все для тестових випадків у .javaфайлі, замість того, щоб вести окремий вміст програми лише для моїх тестів.

Я сподіваюся використовувати Mockito для макетних об'єктів. Раніше я робив це, використовуючи org.mockito.Mockitoта створюючи свої об'єкти за допомогою Mockito.mock(MyClass.class).

Відповіді:


84

Ви можете абсолютно вводити знущання на MyLauncher у своєму тесті. Я впевнений, що якщо ви покажете, який глузливий фреймворк ви використовуєте, хтось може швидко дати відповідь. З mockito я б розглянув питання використання @RunWith (MockitoJUnitRunner.class) та використання анотацій для myLauncher. Це буде виглядати приблизно так, як показано нижче.

@RunWith(MockitoJUnitRunner.class)
public class MyLauncherTest
    @InjectMocks
    private MyLauncher myLauncher = new MyLauncher();

    @Mock
    private MyService myService;

    @Test
    public void someTest() {

    }
}

@ jpganz18 з junit, яким ти зазвичай користуєшся, це все одно, що викликати MockitoAnnotations.initMocks перед кожним методом
fd8s0

Зараз я використовую цей бігун: @RunWith (SpringRunner.class) Чи можу я замінити MockitoJUnitRunner? Якщо ні, чи є спосіб вколоти глузування без нього? (Я насправді не використовую фіктивні об'єкти. Я хочу ввести той самий Beans, який використовує додаток.)
MiguelMunoz

56

Прийнята відповідь (використання MockitoJUnitRunnerта @InjectMocks) чудова. Але якщо ви хочете щось трохи легше (без спеціального бігуна JUnit) і менш "чарівне" (більш прозоре), особливо для випадкового використання, ви можете просто встановити приватні поля безпосередньо за допомогою самоаналізу.

Якщо ви використовуєте Spring, у вас вже є клас корисності для цього: org.springframework.test.util.ReflectionTestUtils

Використання досить просте:

ReflectionTestUtils.setField(myLauncher, "myService", myService);

Перший аргумент - це ваш цільовий компонент, другий - ім'я поля (як правило, приватне), а останній - значення для введення.

Якщо ви не використовуєте Spring, досить просто впровадити такий метод утиліти. Ось код, який я використовував до того, як знайшов клас Spring:

public static void setPrivateField(Object target, String fieldName, Object value){
        try{
            Field privateField = target.getClass().getDeclaredField(fieldName);
            privateField.setAccessible(true);
            privateField.set(target, value);
        }catch(Exception e){
            throw new RuntimeException(e);
        }
    }

1
Чудова відповідь! Мені довелося включити mvnrepository.com/artifact/org.springframework/spring-test у свій pom.xml, щоб отримати клас ReflectionTestUtils.
user674669

1
Я б сказав, що це жахлива порада. Є щонайменше дві причини, за якими: 1. що-небудь вимагає роздумів, як правило, неприємно пахне і вказує на архітектурні проблеми 2. вся суть ін’єкції залежності полягає в тому, щоб легко замінити ін’єктований об’єкт, тому знову, використовуючи відображення, ви пропустили цей момент
арткошелев

@artkoshelev Я погоджуюсь, що для цілей тестування та "архітектурних" проблем конструкторна інжекція є чистішою та рекомендується. Він також краще грає з Java-конфігурацією. Але якщо ви хочете протестувати деякі існуючі компоненти, які використовують ін’єкцію поля і не можуть або не хочуть їх модифікувати, я б стверджував, що краще написати тест, який використовує відображення в установці, ніж взагалі ніякий тест ... Крім того якщо відображення - це завжди запах коду або проблема архектури, ніж Весна - запах коду, Hibernate - запах коду і так далі ...
П'єр Генрі,

"Крім того, якщо відображення - це завжди запах коду або проблема архітектури, ніж Spring - це запах коду, Hibernate - запах коду і так далі ..." я, звичайно, не говорив про добре перевірені та відомі рамки, але власне написали розробники коду програми.
арткошелев

Я віддаю перевагу цій відповіді.
Андрій Рінея,

17

Іноді ви можете переформатувати, @Componentщоб використовувати інжектор на основі конструктора або сеттера для налаштування вашого тесту (ви можете і все ще покладаєтесь @Autowired). Тепер ви можете створити свій тест повністю без глузливого фреймворку, застосувавши замість нього тестові заглушки (наприклад, Мартін Фаулер MailServiceStub ):

@Component
public class MyLauncher {

    private MyService myService;

    @Autowired
    MyLauncher(MyService myService) {
        this.myService = myService;
    }

    // other methods
}

public class MyServiceStub implements MyService {
    // ...
}

public class MyLauncherTest
    private MyLauncher myLauncher;
    private MyServiceStub myServiceStub;

    @Before
    public void setUp() {
        myServiceStub = new MyServiceStub();
        myLauncher = new MyLauncher(myServiceStub);
    }

    @Test
    public void someTest() {

    }
}

Цей прийом особливо корисний, якщо тест і тестований клас знаходяться в одному пакеті, оскільки тоді ви можете використовувати модифікатор приватного доступу за замовчуванням, щоб запобігти доступу інших класів до нього. Зверніть увагу, що ви все ще можете мати свій виробничий код, src/main/javaале тести в src/main/testкаталогах.


Якщо вам подобається Mockito, тоді ви оціните MockitoJUnitRunner . Це дозволяє робити "чарівні" речі, як @Manuel показав вам:

@RunWith(MockitoJUnitRunner.class)
public class MyLauncherTest
    @InjectMocks
    private MyLauncher myLauncher; // no need to call the constructor

    @Mock
    private MyService myService;

    @Test
    public void someTest() {

    }
}

Крім того, ви можете використати за замовчуванням бігун JUnit і викликати MockitoAnnotations.initMocks () у setUp()методі, щоб дозволити Mockito ініціалізувати анотовані значення. Ви можете знайти більше інформації в javadoc @InjectMocks та у своєму блозі, який я написав.


Чудова відповідь, вприскування конструктора є найкращим способом і чудово працює з Kotlin.
Malkaviano

2

Я новий користувач Весни. Я знайшов інше рішення для цього. Використовуючи відображення та оприлюднюючи необхідні поля та призначайте макетні об’єкти.

Це мій контролер аутентифікації, і він має деякі приватні властивості, що підключаються до мережі.

@RestController
public class AuthController {

    @Autowired
    private UsersDAOInterface usersDao;

    @Autowired
    private TokensDAOInterface tokensDao;

    @RequestMapping(path = "/auth/getToken", method = RequestMethod.POST)
    public @ResponseBody Object getToken(@RequestParam String username,
            @RequestParam String password) {
        User user = usersDao.getLoginUser(username, password);

        if (user == null)
            return new ErrorResult("Kullanıcıadı veya şifre hatalı");

        Token token = new Token();
        token.setTokenId("aergaerg");
        token.setUserId(1);
        token.setInsertDatetime(new Date());
        return token;
    }
}

І це мій тест Junit для AuthController. Я роблю загальнодоступну приватну власність і присвоюю їм фіктивні об’єкти та рок :)

public class AuthControllerTest {

    @Test
    public void getToken() {
        try {
            UsersDAO mockUsersDao = mock(UsersDAO.class);
            TokensDAO mockTokensDao = mock(TokensDAO.class);

            User dummyUser = new User();
            dummyUser.setId(10);
            dummyUser.setUsername("nixarsoft");
            dummyUser.setTopId(0);

            when(mockUsersDao.getLoginUser(Matchers.anyString(), Matchers.anyString())) //
                    .thenReturn(dummyUser);

            AuthController ctrl = new AuthController();

            Field usersDaoField = ctrl.getClass().getDeclaredField("usersDao");
            usersDaoField.setAccessible(true);
            usersDaoField.set(ctrl, mockUsersDao);

            Field tokensDaoField = ctrl.getClass().getDeclaredField("tokensDao");
            tokensDaoField.setAccessible(true);
            tokensDaoField.set(ctrl, mockTokensDao);

            Token t = (Token) ctrl.getToken("test", "aergaeg");

            Assert.assertNotNull(t);

        } catch (Exception ex) {
            System.out.println(ex);
        }
    }

}

Я не знаю переваг та недоліків такого способу, але це працює. Ця техніка має трохи більше коду, але ці коди можна розділити різними методами і т. Д. Є більше хороших відповідей на це питання, але я хочу вказати на інше рішення. Вибачте за мою погану англійську. Всім гарного Java для всіх :)


Я також відносно новачок у Весні. Використання @Autowiredздається , щоб зробити тестування дійсно просто. Я також не впевнений, що це "правильне" рішення, однак.
AnonymousAngelo

Питання про ін’єкцію
MK-rou

2

Я вважаю, що для того, щоб працювати з автоматичним підключенням до вашого класу MyLauncher (для myService), вам потрібно буде дозволити Spring ініціалізувати його, а не викликати конструктор, шляхом автоматичного підключення myLauncher. Після того, як це автоматично підключається (а myService також отримує автоматичне підключення), Spring (1.4.0 і новіші версії) надає анотацію @MockBean, яку ви можете поставити у своєму тесті. Це замінить відповідні окремі боби в контексті на макет цього типу. Потім ви можете додатково визначити, яке глузування ви хочете, за допомогою методу @Before.

public class MyLauncherTest
    @MockBean
    private MyService myService;

    @Autowired
    private MyLauncher myLauncher;

    @Before
    private void setupMockBean() {
        doNothing().when(myService).someVoidMethod();
        doReturn("Some Value").when(myService).someStringMethod();
    }

    @Test
    public void someTest() {
        myLauncher.doSomething();
    }
}

Тоді ваш клас MyLauncher може залишитися немодифікованим, а ваш компонент MyService буде макетом, методи якого повертають значення, як ви визначили:

@Component
public class MyLauncher {
    @Autowired
    MyService myService;

    public void doSomething() {
        myService.someVoidMethod();
        myService.someMethodThatCallsSomeStringMethod();
    }

    //other methods
}

Пара переваг цього перед іншими згаданими методами полягає в тому, що:

  1. Вам не потрібно вводити вручну myService.
  2. Вам не потрібно використовувати бігун Mockito або правила.

Не @MockBeanпотрібно @RunWith(SpringRunner.class)?
wonsuc

1

Подивіться на це посилання

Потім напишіть свій тест як

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"/applicationContext.xml"})
public class MyLauncherTest{

@Resource
private MyLauncher myLauncher ;

   @Test
   public void someTest() {
       //test code
   }
}

Це правильний спосіб завантажити Весняний контекст !! але я думаю, що краще створити контекст тесту для своїх тестів ..
Тіаре Бальбі

Це не обов'язково правильний шлях, все залежить від того, що ви намагаєтесь досягти. Що робити, якщо ви налаштовуєте сеанси глибокого сну в контексті програми? Ви дійсно хочете отримати справжню базу даних за допомогою модульного тесту? Деякі люди можуть сказати "так", не розуміючи, що вони створюють інтеграційний тест і, можливо, викрадають свої дані. З іншого боку, якщо ви створили контекст тестового додатка і вказали сплячий режим на вбудовану базу даних, тоді ваші дані краще, але ви все одно створюєте інтеграційний тест, а не модульний тест.
Дан

Це так звані "інтеграційні тести", коли ви хочете протестувати свої компоненти разом із усім контекстом. Це, звичайно, корисно, але не те саме, що модульний тест, коли ви хочете перевірити окремий клас ізольовано.
Pierre Henry
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.