UnitTest и PowerMock в Java

Java модульный тест

UnitTest и PowerMock

При изучении компьютерного языка, я думаю, помимо изучения его синтаксиса, самое главное научиться выполнять модульное тестирование в этой языковой среде, потому что модульное тестирование может помочь вам найти ошибки на ранней стадии; в то же время добавить защитный net к вашей программе, чтобы ваша модификация не разрушила исходную функцию; модульное тестирование также может помочь вам написать лучший код, в конце концов, код, который нельзя протестировать, не должен быть хорошим кодом; кроме того, это также может повысить вашу уверенность , смело могу сказать "в моей программе нет багов".

Каждый язык имеет свою общую структуру модульного тестирования.В этой статье в основном рассказывается, как мы используем PowerMock в Java для решения проблем, с которыми мы сталкиваемся при написании модульных тестов.Из слова Mock мы видим, что такие проблемы в основном решают проблему зависимости.

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

Библиотека PowerMock на Java — это очень мощная библиотека зависимостей.Три функции, упомянутые ниже, могут помочь вам решить большинство проблем:

  1. Внедрение зависимых объектов через PowerMock
  2. Моделирование статических функций с помощью PowerMock
  3. Как имитировать выходные параметры

Внедрение зависимых объектов через PowerMock

Предположим, у вас есть два класса,MyServiceа такжеMyDao,MyServiceзависит отMyDao, и они определяются следующим образом

// MyDao.java
@Mapper
public interface MyDao {
    /**
     * 根据用户 id 查看他最近一次操作的时间
     */
    Date getLastOperationTime(long userId);
}

// MyService.java
@Service
public class MyService {
	@Autowired
	private MyDao myDao;
	
    public boolean operate(long userId, String operation) {
        Date lastTime = myDao.getLastOperationTime(userId);
        // ...
    }
}

Эта услуга предоставляетoperateинтерфейс, пользователь будет ограничен одной рабочей частотой при вызове этого интерфейса, поэтому система будет записывать время последней операции каждого пользователя черезMyDao.getLastOperationTime(long userId)Приобретение интерфейса, теперь мы должныMyServiceКатегорияoperateКак проводить модульное тестирование?

Вы можете подумать об использовании SpringBoot, который может автоматически инициализироваться для нас.myDaoобъект, но есть некоторые проблемы с этим:

  1. SpringBoot запускается медленно, что может увеличить время модульного тестирования.
  2. Поскольку время — это постоянно меняющаяся величина, может быть, время, которое вы сконструировали, на этот раз удовлетворяет условию теста, но в следующий раз, когда вы запустите тест, оно может не соответствовать условию.

По вышеуказанным причинам мы обычно не запускаем контекст SpringBoot при модульном тестировании, а используем PowerMock, чтобы помочь нам внедрить зависимости.Для приведенного выше случая наш тестовый пример можно записать следующим образом:

// MyServiceTest.java
@RunWith(PowerMockRunner.class)
@PrepareForTest({MyService.class, MyDao.class})
public class MyServiceTest {
    @Test
    public void testOperate() throws IllegalAccessException {
        // 构造一个和当前调用时间永远只差 4 秒的返回值
    	Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.SECOND, -4);
        Date retTime = calendar.getTime();
        
        // spy 是对象的“部分 mock”
        MyService myService = PowerMockito.spy(new MyService());
        MyDao md = PowerMockito.mock(MyDao.class);
        PowerMockito
                .when(md.getLastOperationTime(Mockito.any(long.class)))
                .thenReturn(retTime);
        // 替换 myDao 成员
        MemberModifier.field(MyService.class, "myDao").set(myService, md);
        // 假设最小操作的间隔是 5 秒,否则返回 false
        Assert.assertFalse(myService.operate(1, "test operation"));
    }
}

Из приведенного выше кода мы сначала строим время возвратаretTime, имитируемый интервал работы составляет 4 секунды, что гарантирует, что условие не изменится при каждом запуске теста, тогда мы используемspyпостроить тестMyServiceобъект,spyа такжеmockРазница в том,spyТолько частично смоделированные объекты, то есть только модифицированные здесьmyService.myDaoчленов, остальные остаются прежними.

Затем мы определяем издевательский объектMyDao mdвызывающее поведение, когдаmd.getLastOperationTimeКогда функция вызывается, верните время, которое мы построилиretTime, тестовая среда настроена на этом этапе, после этого вы можете легко протестироватьoperateфункция.

Моделирование статических функций с помощью PowerMock

Упомянутое выше использование PowerMock для внедрения зависимостей может охватывать большинство сценариев зависимости в тесте, и еще одна распространенная зависимость — это статические функции, такие как некоторые из наших собственных.CommonUtilфункции в служебном классе.

Продолжая использовать приведенный выше пример, предположим, что мы хотим вычислить интервал между текущим временем и временем последнего действия пользователя и использоватьpublic static long getTimeInterval(Date lastTime)Реализуйте эту функцию следующим образом:

// CommonUtil.java
class CommonUtil {
    public static long getTimeInterval(Date lastTime) {
        long duration = Duration.between(lastTime.toInstant(),
                new Date().toInstant()).getSeconds();
        return duration; 
    }
}

нашoperatorФункция изменена следующим образом

// MyService.java
// ...
    public boolean operate(long userId, String operation) {
        Date lastTime = myDao.getLastOperationTime(userId);
        long duration = CommonUtil.getTimeInterval(lastTime);
        if (duration >= 5) {
            System.out.println("user: " + userId + " " + operation);
            return true;
        } else {
            return false;
        }
    }
// ...

Здесь изmyDaoПолучите время последней операции, затем вызовитеCommonUtil.getTimeIntervalВычислить интервал операции и вернуться, если он меньше 5 секундfalse, иначе выполнить операцию и вернутьсяtrue. Итак, мой вопрос: как здесь избавиться от зависимости статической функции? Давайте посмотрим непосредственно на тестовый код

// MyServiceTest.java
@PrepareForTest({MyService.class, MyDao.class, CommonUtil.class})
public class MyServiceTest {
// ...
    @Test
    public void testOperateWithStatic() throws IllegalAccessException {
        // ...
        PowerMockito.spy(CommonUtil.class);
        PowerMockito.doReturn(5L).when(CommonUtil.class);
        CommonUtil.getTimeInterval(Mockito.anyObject());
        // ...
    }
}

Сначала в аннотации@PrepareForTestувеличить вCommonUtil.class, все еще используюspyпарный классCommonUtilСделайте макет, если вы этого не сделаете, поведение всех статических функций в этом классе изменится, что вызовет проблемы для ваших тестов.spyСледующие две строки кода следует читать вместе, это означает, что при вызовеCommonUtil.getTimeInterval, возвращает 5; это странный способ записи, но он требуется для PowerMock. К этому моменту вы овладели искусством имитации статических функций.

Как имитировать выходные параметры

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

// MyTask.java
// ...
    public boolean run() throws InterruptedException {
        while (true) {
            updateStatus(operation);

            if (operation.getStatus().equals("success")) {
                return true;
            } else {
                Thread.sleep(1000);
            }
        }
    }

    public void updateStatus(Operation operation) {
        String status = myDao.getStatus(operation.getOperationId());
        operation.setStatus(status);
    }
// ...

В приведенном выше кодеrun()— это задача опроса, которая постоянно обновляет статус операции, и когда статус достигает"success"остановись, когда увидишь,updateStatusЭто то, что мы называем функцией.Хотя у нее нет возвращаемого значения, она изменяет объект, на который ссылается параметр, поэтому такой тип параметра также называется выходным параметром.

Сейчас мы будем тестироватьrun()поведение функции, чтобы увидеть, будет ли она"success"Выход в состоянии, тогда нам нужен mockupdateStatusфункцию, что делать? Вот его тестовый код:

    @Test
    public void testUpdateStatus() throws InterruptedException {
        // 初始化被测对象
        MyTask myTask = PowerMockito.spy(new MyTask());
        myTask.setOperation(new MyTask.Operation());
        // 使用 doAnswer 来 mock updateStatus 函数的行为
        PowerMockito.doAnswer(new Answer<Object>() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                Object[] args = invocation.getArguments();
                MyTask.Operation operation = (MyTask.Operation)args[0];
                operation.setStatus("success");
                return null;
            }
        }).when(myTask).updateStatus(Mockito.any(MyTask.Operation.class));

        Assert.assertEquals(true, myTask.run());
    }

В приведенном выше коде мы используемdoAnswerиздеватьсяupdateStatusповедение, эквивалентное использованиюanswerфункция замены оригиналаupdateStatusфункция, здесь мы будемoperationСтатус установлен на"success", ждатьmyTask.run()возврат функцииtrue. Итак, мы снова научились мокать функции с выходными параметрами.

Приведенный выше код предназначен только для иллюстрации сценария приложения, а не кода уровня производственной среды, и все они прошли тест, для удобства последующего обучения вы можете скачать его здесь:GitHub.com/Джи Ниу/Арити…

Ссылаться на: