Как может модульное тестирование "проверить контракт" на методе, который возвращает void?
Java 8 здесь, но это общий вопрос модульного тестирования, который (вероятно) не зависит от языка.
Синтаксис написания теста JUnit прост, но решение о том, какие тесты писать и как тестировать основной / производственный код, является для меня самой большой проблемой. Читая о передовых практиках модульного тестирования, я постоянно слышу одно и то же:
Проверить контракт
Я считаю, что идея заключается в том, что модульные тесты не должны быть хрупкими и не обязательно ломаться, если изменяется реализация метода. Что метод должен определять контракт входных данных -> результаты / результаты, и что тесты должны быть направлены на проверку выполнения контракта. Я думаю.
Допустим, у меня есть следующий метод:
public void doFizzOnBuzz(Buzz buzz, boolean isFoobaz) {
// wsClient is a REST client for a microservice
Widget widget = wsClient.getWidgetByBuzzId(buzz.getId());
if(widget.needsFile()) {
File file = readFileFromFileSystem(buzz.getFile());
if(isFoobaz) {
// Do something with the file (doesn't matter what)
}
}
return;
}
private File readFileFromFileSystem(String filename) {
// Private helper method; implementation doesn't matter here EXCEPT...
// Any checked exceptions that Java might throw (as a result of working)
// with the file system are wrapped in a RuntimeException (hence are now
// unchecked.
// Reads a file from the file system based on the filename/URI you specify
}
Итак, у нас есть метод, для которого мы хотим написать модульные тесты (doFizzOnBuzz
). Этот метод:
- Имеет два параметра,
buzz
а такжеisFoobaz
- Использует свойство класса
wsClient
позвонить по сети /REST - Вызывает закрытый вспомогательный метод, который не только работает с внешней файловой системой, но и "проглатывает" проверенные исключения; следовательно
readFileFromFileSystem
мог броситьRuntimeExceptions
Какие виды модульных тестов мы можем написать для этого, чтобы "проверить контракт"?
Проверка входных данных (buzz
а также isFoobaz
) очевидны; контракт должен определять, какие допустимые значения / состояния для каждого из них, и какие исключения / результаты должны произойти, если они недействительны.
Но кроме этого, я не совсем уверен, какой бы здесь был "контракт", что очень затрудняет написание тестов. Поэтому я думаю, что этот вопрос действительно должен звучать примерно так: "Как мне определить, что такое контракт для модульного теста, и как вы пишете тесты, нацеленные на контракт, а не на реализацию?"
Но этот заголовок был бы слишком длинным для ТАКОГО вопроса.
2 ответа
Ваш код с методами doFizzOnBuzz(Buzz buzz, boolean isFoobaz)
а также private File readFileFromFileSystem(String filename)
не легко проверить, потому что первый метод попытается прочитать файл, а это не то, что вы хотите сделать в тесте.
Вот, doFizzOnBuzz
нужно что-то, чтобы предоставить файл для работы с ним. это FileProvider
(как я назову это) может быть интерфейсом, что-то вроде:
public interface FileProvider {
File getFile(String filename);
}
При работе в производстве используется реализация для фактического чтения файла с диска, но при модульном тестировании doFizzOnBuzz
фиктивная реализация FileProvider
может быть использован вместо Это возвращает насмешку File
,
Ключевым моментом, который следует помнить, является то, что при тестировании doFizzOnBuzz
Мы не проверяем то, что предоставляет файл, или что-то еще. Мы предполагаем, что работать правильно. Эти другие биты кода имеют свои собственные модульные тесты.
Фреймворк Mockito, такой как Mockito, может быть использован для создания FileProvider
а также File
и вводить макет FileProvider
в тестируемый класс, возможно, используя сеттер:
public void setFileProvider(FileProvider f) {
this.fileProvider = f;
}
Кроме того, я не знаю, что wsClient
есть, я знаю, что у него есть getWidgetByBuzzId()
метод. Этот класс тоже мог бы быть интерфейсом, и для целей тестирования интерфейс был бы смоделирован и возвращал макет Widget
, аналогично FileProvider выше.
С помощью mockito вы можете не только устанавливать фиктивные реализации интерфейсов, но и определять, какие значения возвращаются при вызове методов на этом интерфейсе: например,
//setup mock FileProvider
FileProvider fp = Mockito.mock(FileProvider.class);
//Setup mock File for FileProvider to return
File mockFile = Mockito.mock(File.class);
Mockito.when(mockFile.getName()).thenReturn("mockfilename");
//other methods...
//Make mock FileProvider return mock File
Mockito.when(fp.getFile("filename")).thenReturn(mockFile);
ClassUnderTest test = new ClassUnderTest();
test.setFileProvider(fp); //inject mock file provider
//Also set up mocks for Buzz,, Widget, and anything else
//run test
test.doFizzOnBuzz(...)
//verify that FileProvider.getFile() was actually called:
Mockito.verify(fp).getFile("filenane");
Вышеприведенный тест не пройден, если getFile() не был вызван с параметром filename
Заключение Если вы не можете непосредственно наблюдать результаты метода, например, он является недействительным, вы можете использовать Mocking для проверки его взаимодействия с другими классами и методами.
Проблема в том, что ваш метод контракта не говорит, какой эффект вы можете наблюдать со стороны. По сути, это BiConsumer, так что, если вы хотите убедиться, что есть исключение или нет, не так уж много юнит-тестирования.
Тест, который вы можете сделать, - убедиться, что вызывается служба (Mocked) REST, или что метод (часть параметра Buzz, который может указывать на временный файл) будет затронут методом при некоторых условиях.
Если вы хотите выполнить модульное тестирование выходных данных метода, вам может потребоваться рефакторинг, чтобы отделить определение того, что должно быть сделано (файл нуждается в обновлении) от фактического выполнения этого.