Hammerhall EN Сколько стоит Войти

Тест, который ничего не проверяет

Статья 5 · читать минут 30

Для кого. Ты прочитал статьи 1–4 этой серии: у тебя есть песочница testy, таблица случаев к зачёту по сроку и привычка ломать код руками. Серия «От Java к Spring Boot» не нужна.

Что будет. Как выглядит тест, который не умеет покраснеть, и как его починить; что показывает покрытие JaCoCo и о чём оно молчит; как проверить сам тест — сломать код по общему списку поломок.

Откуда статья. Раздел «Буткемп / Лаборатория» — о том, как пишут код команды Hammerhall: буткемп работает в открытых репозиториях, лаборатория — в закрытых. Серия «От красного к зелёному» — как работать с тестами на проекте: писать их, читать красную сборку и верить зелёному тесту, только если видел его красным. Примеры — учебные: их можно повторить у себя.


Зачем проверять тесты#

В статье 1 «Зачем тесты и чего от тебя ждут на проекте» осталось обещание: тест, который ни разу не краснел, может и вовсе ничего не проверять. Эта статья — о таких тестах.

Тест — тоже программа, и ошибки в нём тоже бывают. Только ошибка в коде делает тест красным, а ошибка в тесте обычно не делает красным ничего. Такой тест останется зелёным, даже когда код сломают. Со стороны он выглядит защитой, как тест, который сборка не запускает (статья 1).

Пример прежний: зачёт по сроку, DeadlineScore и итоговые тесты статьи 4 «Какие случаи проверять». Напишем к нему тесты, которые не проверяют ничего, и посмотрим на них глазами покрытия. Потом испортим код пятью способами и узнаем, кто это заметит.

Ломать код и смотреть, кто покраснеет, ты уже пробовал в статьях 2 и 4. Сегодня у приёма появится имя, а поломки сложатся в общий список, который обещала статья 4.


Часть 1. Четыре способа быть зелёным всегда

Вот четыре теста на DeadlineScore из учебного файла EmptyTest. Все зелёные, и писать так не надо; целиком файл — в конце статьи. DEADLINE — та же константа, что в статье 2: полночь 13 октября по Москве. Строк import в коротких примерах, как всегда, нет.

Вызвал — и не проверил#

    @Test
    void noAssertion() {
        DeadlineScore.score(DEADLINE, DEADLINE);                    // вызвали — и всё
    }

Тест вызывает код и выбрасывает результат. Проверки нет, значит, падать нечему. Вернёт score 100, 50 или минус сорок — тест зелёный. Покраснеть он может только от исключения, которое вылетит по дороге.

Починка — ожидание, записанное в самом тесте числом, как в статье 2:

    @Test
    void onDeadlineIsStillOnTime() {
        int score = DeadlineScore.score(DEADLINE, DEADLINE);

        assertThat(score).isEqualTo(100);
    }

Ждал исключение — и проглотил#

    @Test
    void swallowsException() {
        try {
            DeadlineScore.score(null, DEADLINE);
        } catch (IllegalArgumentException expected) {
            // ждали исключение — и молча проглотили; а если его не будет?
        }
    }

Задумано верно: без срока score обязан бросить исключение. Бросит — catch его поймает, и тест зелёный. Не бросит — try закончится спокойно, и тест снова зелёный. Оба исхода дают один цвет. Значит, тест не отличает правильный код от сломанного.

Починка — assertThatThrownBy из статьи 4. Он сам вызывает код через лямбду () ->, сам ловит исключение и падает, если исключения не было. Заодно проверяет тип и текст:

    @Test
    void noDeadlineIsAnError() {
        assertThatThrownBy(() -> DeadlineScore.score(null, DEADLINE))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("Срок не указан");
    }

⚠️ В старых примерах ты увидишь другой способ: в try сразу за вызовом стоит fail(), который роняет тест, а catch пустой. Дошли до fail() — значит, исключения не было. Способ работает, пока fail() на месте. Забыл одну строку — получил swallowsException. В assertThatThrownBy забыть нечего.

Сверил код с самим собой#

    @Test
    void agreesWithItself() {
        Instant hourLate = DEADLINE.plus(Duration.ofHours(1));

        assertThat(DeadlineScore.score(DEADLINE, hourLate))
                .isEqualTo(DeadlineScore.score(DEADLINE, hourLate));   // код сверяется сам с собой
    }

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

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

Ожидание записывают независимо от кода: из правила, из задачи, руками. Для опоздания на час правило говорит «50», и тест статьи 2 так и пишет: assertThat(score).isEqualTo(50).

Проверил то, что верно всегда#

    @Test
    void alwaysTrue() {
        assertThat(DeadlineScore.score(DEADLINE, DEADLINE)).isBetween(0, 100);   // верно при любом правиле
    }

isBetween(0, 100) значит «от 0 до 100 включительно». Баллов у зачёта три: 100, 50 и 0, и все лежат между нулём и сотней. Перепутай границу, сдвинь сутки, отдай опоздавшему полный балл. Балл останется одним из трёх, и проверка будет истинной.

Так же устроены «не null» и «список не пуст». Если ответ известен точно, проверяй точно: «сдал ровно в срок — 100». Это строка PT0S | 100 в таблице статьи 4.

Проверить тест на пустоту можно, не запуская его. Придумай правдоподобную ошибку в правиле, какую мог бы сделать коллега, и спроси: покраснеет ли тест? У всех четырёх ответ «нет».

Помощник, который согласился с проверяемым

Тест, согласный сам с собой, умеет прятаться в одну удобную строку. В рабочем проекте так вышло с защитой от подделанных запросов. Сервер выдаёт браузеру секретный знак, а браузер прикладывает его к каждому запросу, который что-то меняет.

В тестах знак подкладывал помощник из тестовой библиотеки, одной строкой на тест. Брал он знак изнутри, у того же механизма сервера, и подавал ровно так, как механизм ждёт. Однажды механизм настроили не до конца, и знак от настоящего браузера сервер разобрать не мог. Регистрация из браузера не работала. А 429 тестов, включая тесты регистрации, были зелёными: заметил это человек, отправив запрос руками.

Хуже того, помощник подменял механизм на весь прогон. Правильный тест, идущий путём браузера, в полном прогоне краснел. И рука тянулась «починить» его тем же помощником.

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


Часть 2. Покрытие: где тесты побывали

В итогах прогона пустой тест не отличить: он такой же зелёный, как все. Нужен инструмент, который смотрит внутрь.

JaCoCo в песочнице#

Покрытие — доля кода, которую тесты исполнили (статья 1). Считает его JaCoCo, плагин Maven, как Surefire. Во время прогона он отмечает исполненные строки и пройденные ветви, то есть дороги if: «да» и «нет». Как маркер, которым ведут по пройденным строкам: после прогона видно, где тесты были, а где ни разу.

В учебных проектах JaCoCo почти не встретишь, а на работе без него редко обходятся. Тестов там сотни, и никто не помнит, куда заходит каждый. JaCoCo разобран в статье 0 «Что нужно знать до Spring» цикла «От Java к Spring Boot». Статьи 1 и 4 обещали вернуться к нему подробнее, и здесь он — с нуля.

Ставить его отдельно не нужно: несколько строк в pom.xml, и Maven сам его скачает. В рабочем проекте он уже подключён. В песочницу добавим сами, рядом с плагином, который положил Initializr:

<!-- pom.xml песочницы testy: что добавляет статья 5 — внутри <build><plugins> -->
<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.15</version>
    <executions>
        <execution>
            <goals>
                <goal>prepare-agent</goal>                  <!-- подключить счётчик к прогону тестов -->
            </goals>
        </execution>
        <execution>
            <id>report</id>
            <phase>verify</phase>
            <goals>
                <goal>report</goal>                         <!-- отчёт в target/site/jacoco -->
            </goals>
        </execution>
    </executions>
</plugin>

Каждая работа плагина — блок <execution>. В <goal> сказано, что сделать, в <phase> — на каком шаге сборки, а <id> — имя блока. Работ здесь две.

prepare-agent встаёт на свой шаг по умолчанию, ещё до тестов. Он подключает к их прогону агента: маленькую программу, которую Java загружает вместе с тестами. Агент и ведёт маркером. report на шаге verify собирает из отмеченного отчёт.

⚠️ В интернете ты увидишь примеры с JaCoCo 0.8.8 или 0.8.11. Официально Java 25 поддерживается с JaCoCo 0.8.14, а старые версии спотыкаются о классы новых JDK. В песочнице — 0.8.15, как и в рабочем проекте.

Запуск — ./mvnw clean verify. Отчёт — файл target/site/jacoco/index.html, открой его в браузере.

⚠️ clean здесь не для порядка. JaCoCo дописывает отметки нового прогона к старым, если они остались в target. Без clean в отчёт попадёт и то, что исполняли тесты прошлых запусков.

Как читать отчёт#

Отчёт — страницы, вложенные друг в друга: пакеты, классы пакета, методы класса, исходник. У каждого пакета и класса в таблице два процента (Cov.). Первый — по инструкциям: это мелкие шаги, на которые Java раскладывает каждую строку кода. Второй — по ветвям. Строки даны числами: сколько пропущено (Missed) из скольких (Lines).

Нажми на пакет example.testy.score, потом на DeadlineScore. Откроется твой код, раскрашенный построчно:

У строк с if слева стоит ромбик того же цвета. Зелёный значит, что тесты прошли обе ветви, жёлтый — одну, красный — ни одной.

⚠️ Красное в отчёте — не «тест упал», а «сюда тесты не заходили».

Сто процентов без единой проверки

Вот второй учебный файл, HollowTest. Тест в нём написан нарочно. Он заходит в каждую ветвь score: вовремя, половина, ноль, нет срока, нет времени сдачи. И не проверяет ничего.

class HollowTest {

    private static final Instant DEADLINE = Instant.parse("2026-10-12T21:00:00Z");

    @Test
    void touchesEveryLine() {
        DeadlineScore.score(DEADLINE, DEADLINE);                            // ветвь «вовремя»
        DeadlineScore.score(DEADLINE, DEADLINE.plus(Duration.ofHours(1)));  // ветвь «половина»
        DeadlineScore.score(DEADLINE, DEADLINE.plus(Duration.ofDays(2)));   // ветвь «ноль»
        try {
            DeadlineScore.score(null, DEADLINE);                            // ветвь «нет срока»
        } catch (IllegalArgumentException ignored) {
        }
        try {
            DeadlineScore.score(DEADLINE, null);                            // ветвь «нет времени сдачи»
        } catch (IllegalArgumentException ignored) {
        }
    }
}

Запусти только его: ./mvnw clean verify -Dtest=HollowTest, где -Dtest запускает один класс (статья 1). У DeadlineScore в отчёте 100% инструкций и ветвей и ни одной пропущенной строки из десяти. Всё зелёное, все ромбики зелёные. Остальные классы песочницы красные: их этот тест не трогал.

Запусти так же итоговые тесты статьи 4, -Dtest=DeadlineScoreTest. Будет то же самое: отчёт не отличит пустой тест от честного. А четыре теста из части 1 (-Dtest=EmptyTest) оставят красными две строки из десяти, «нет времени сдачи» и «ноль». Ромбики у двух условий будут жёлтыми: ветвей пройдено 75%.

🔑 Покрытие говорит, что код исполнялся, а не что он проверен. JaCoCo видит, где тесты были, и не знает, что они там проверили.

96% и экран, который врал#

В браузерной части рабочего проекта был экран, который показывал человеку с десятью решёнными задачами: «ни одной задачи не засчитано». У экрана было больше двухсот пятидесяти зелёных тестов и 96% покрытия.

Тесты подменяли ответы сервера, а придумывал эти ответы тот же человек, который писал экран. Врал же экран на настоящих данных, которых в придуманных ответах не было: у части задач нужных ему сведений нет вовсе. Экран принял «сведений нет» за «ничего не решено».

Вывод там записали так: покрытие меряет, сколько строк исполнилось, а не сколько состояний бывает у данных.

Непокрытая ветвь — это вопрос#

Значит, покрытие бесполезно? Нет. На «проверено ли» оно отвечает плохо, а на «не проверено ли» — точно. Красная строка доказывает, что код не проверен: тест её не исполнил, значит, и проверить не мог.

Каждая красная строка и каждый жёлтый ромбик — вопрос. Ответов у него два:

Дыру назвал отчёт, а не прогон

Однажды в команде после зелёного прогона в отчёте появился класс с нулём процентов. Ручку, которая показывает роли человека, тесты звали только для человека без ролей. Код, превращающий роли в ответ, не исполнился ни разу. Переставь в ответе два поля местами, и ни один из 610 тестов этого бы не заметил.

Бывает и второй ответ: непокрытой оказывается защитная проверка, до которой не дойти никаким входом. Мёртвый код в отчёте выглядит забытым случаем, но его нельзя ни выполнить, ни проверить. Он тянет цифру вниз и ничего не защищает.

Отсюда привычка. После своей правки смотри не только на процент: один нулевой класс среди сотни его почти не двигает. Смотри на имена: какие классы и ветви остались красными.

Порог ловит не всё#

В команде у JaCoCo есть третья работа, check. На шаге verify она берёт весь проект и требует не меньше 80% строк и 70% ветвей (статья 1). Ниже порога ./mvnw verify красный, и пул-реквест не вливают. Новый код задачи там покрывают полностью.

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

Будь DeadlineScore всем проектом, четыре пустых теста прошли бы порог 80/70. Восемь строк из десяти дают ровно 0,80, а ветвей пройдено 75%. HollowTest прошёл бы даже требование к новому коду. Порог ловит код, куда тесты не заходили. Что они там проверили — вопрос к тебе и к ревьюеру.


Часть 3. Сломай руками#

Мутант#

Проверить тест можно одним способом: дать ему повод покраснеть. В статьях 2 и 4 ты так и делал: портил код одной правкой и смотрел, кто покраснеет. У испорченной копии кода есть имя — мутант.

Тест покраснел — мутант убит: такую ошибку тест замечает. Все зелёные — мутант выжил, и это вопрос к тестам.

Вот обещанный в статье 4 общий список: пять мутантов DeadlineScore, каждый прогнан на всех тестах серии. Откуда они: M1 и M2 из статьи 1, M3 из статьи 2, M4 и M5 из статьи 4.

мутант что испортили 4 «пустых» HollowTest 4 теста статьи 2 итоговые тесты статьи 4
M1 !isAfter(deadline) → isBefore(deadline): сдавший ровно в срок уже опоздал зелёные зелёный красный onDeadlineIsStillOnTime красная строка PT0S
M2 GRACE: 24 часа → 23 зелёные зелёный зелёные красная строка PT24H
M3 во второй ветви HALF → FULL: опоздавшим полный балл зелёные зелёный красный lateWithinDayGetsHalf красные строки PT0.000000001S, PT20M, PT1H, PT24H, тест полуночи и singleAttemptCountsAsIs из AttemptsTest
M4 нет срока — return FULL вместо исключения зелёные зелёный зелёные красный noDeadlineIsAnError
M5 !isAfter(срок + сутки) → isBefore(…): ровно через сутки — уже ноль зелёные зелёный зелёные красная строка PT24H

Что видно из таблицы:

Покрытие у HollowTest и у итоговых тестов одинаковое. Мутанты делят их честно: ноль из пяти и пять из пяти.

Как ломать#

  1. Одна правка за раз, иначе не поймёшь, какую из них поймали.
  2. Запусти тесты: ./mvnw test. Красными должны стать те, что стерегут испорченное место.
  3. Верни правку и убедись, что всё снова зелёное. Мутант, забытый в пул-реквесте, хуже любого пустого теста.
  4. Мутант выжил — подумай. Если такую ошибку стоит ловить, не хватает случая: допиши его и сломай снова. Если правка поведения не меняет, ловить нечего.

Где ломать: на границах (isAfter ↔︎ isBefore, > ↔︎ >=), в константах, в возвращаемом значении, там, где бросается исключение. Там и прячутся ошибки, которые пропускают тесты.

Так мысль из статьи 1 становится рабочим приёмом. Написал тест — сломай код, который он стережёт, увидь красное и верни.

💡 Ломать руками долго, и мутантов будет ровно столько, сколько ты придумаешь. PIT (pitest) делает то же автоматом: сам вносит в код мелкие правки, на каждой гоняет тесты и считает, сколько мутантов убито и сколько выжило. Его сайт говорит прямо: обычное покрытие «измеряет только, какой код исполнили тесты», а качество тестов видно по доле убитых мутантов. В рабочем проекте подключить его — решение мидла, разработчика с опытом. Тебе хватит знать, что так можно, а руками ты уже умеешь.

Отрицательный и положительный контроль

Отрицательный контроль — нарочно нарушить то, что тест стережёт, и убедиться, что он покраснел. Под этим именем приём есть и в статье 11 «Тесты-сторожа» цикла «От Spring к системе». Каждый мутант выше — отрицательный контроль.

Положительный контроль нужен проверкам наоборот, вида «X не случилось»: «отказ», «ошибки нет», «ничего не изменилось». Такая проверка истинна и тогда, когда механизм не работает вовсе. Выключенный счётчик тоже «не вырос». Поэтому рядом ставят доказательство, что X умеет случаться: тот же путь, где X разрешён, и та же проверка его видит.

«Не случилось» истинно и при поломке

В команде это записано правилом: рядом с каждым «X не произошло» обязано стоять доказательство, что X вообще умеет происходить.

Строже всего там с правами. Ответ «отказ» истинен и тогда, когда операция успела сработать, а отказ пришёл после неё. Поэтому к каждому «не пустило» там добавляют «и ничего не произошло»: доступ прежний, записей в журнале действий ноль. Иначе доступ, который тихо выдали, не виден ничем.


Часть 4. Проверять следствие#

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

«Проверено следствием»#

Возьмём проверку списка на Java. Метод идёт по сдачам группы, сверяет балл каждой с правилом и считает: проверенные в checked, а упавшие, где балл не сошёлся, в failed. Итог «всё в порядке», если failed == 0.

Почему этого мало? «Упавших ноль» истинно и тогда, когда сдач нет вовсе: в пустом списке упавших тоже ноль. Список могли не загрузить или загрузить не тот, а проверка всё равно зелёная. Поэтому условие пишут так: проверенных больше нуля и упавших ноль, checked > 0 && failed == 0. Первая половина — положительный контроль из части 3, одной строкой.

Похожая ловушка — верить ответу «я здоров». Метод загрузки, который вернул true, говорит только о себе: он отработал без ошибок. Есть ли в списке хоть одна сдача, этот ответ не скажет. Поэтому проверяют не ответ, а следствие: сдачи на месте, и балл у каждой по правилу.

Так и в части 1. noAssertion похож на ответ «я здоров»: код вызвался и не упал. А тест с точным ожиданием проверяет следствие: сдал ровно в срок — 100.

Зелёный тест и доля, равная нулю

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

Вопрос теста такой: если хранилище скажет, что дошли 3 из 10, получится ли 30,0? Это арифметика, и она верна. Тест зелёный, а на настоящем экране доля — ноль.

Вопрос дела другой: какая доля людей на самом деле дошла? Чтобы на него ответить, кто-то должен ставить заявке последний статус. Вручную его не поставить, так задумано: этот статус ставит система. А код, который его ставил бы, не написали.

Заметить это тест не может. «Дошли — 3» он придумал сам: вместо настоящего хранилища подставил заглушку, объект с заранее готовым ответом. О заглушках — в продолжении серии. Интеграционный тест того же экрана, с настоящей базой, тоже не может: он кладёт в базу заявку сразу в нужном статусе, в обход кода.

Состояние, которое в жизни получается только после работы другого кода, тест подложил руками. Урок тот же, что у экрана из части 2. И то же правило из статьи 1: зелёный тест отвечает ровно на тот вопрос, который ему задали. Тест с вопросом дела был бы красным с первого дня: «человек дошёл до регистрации — попал ли он в долю?»


Проверь себя#

Ответь своими словами — вслух или на бумаге. Не получается — перечитай раздел.

  1. swallowsException и noDeadlineIsAnError ждут одного и того же исключения. Почему первый зелёный и при сломанном коде, а второй — нет?
  2. Почему ожидание в тесте нельзя брать из той же константы или считать той же формулой, что в коде?
  3. Как, не запуская тест, понять, что он не заметит ошибки? Какой вопрос себе задать?
  4. HollowTest проходит каждую строку и ветвь DeadlineScore. Что это доказывает и чего не доказывает?
  5. В отчёте JaCoCo красная строка. Какие два ответа бывают у этого вопроса и что делать в каждом случае?
  6. Что такое мутант? Почему M2 выжил на четырёх тестах статьи 2, но не на итоговых тестах статьи 4?
  7. Чем положительный контроль отличается от отрицательного? Почему проверке «отказ» нужен положительный?
  8. Что значит «проверено следствием»? Почему тест доли из части 4, хороший по всем признакам этой статьи, не заметил нуля на экране?

Что дальше#

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

Первоисточники#


Пример целиком — с импортами#

Учебный пример живёт в песочнице testy из статьи 1. К этой статье в ней всё из статей 1–4: DeadlineScore и Attempts, итоговые DeadlineScoreTest и AttemptsTest. Новых зависимостей нет, добавляется только плагин JaCoCo в pom.xml, внутри <build><plugins>:

<!-- pom.xml песочницы testy: что добавляет статья 5 — внутри <build><plugins> -->
<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.15</version>
    <executions>
        <execution>
            <goals>
                <goal>prepare-agent</goal>                  <!-- подключить счётчик к прогону тестов -->
            </goals>
        </execution>
        <execution>
            <id>report</id>
            <phase>verify</phase>
            <goals>
                <goal>report</goal>                         <!-- отчёт в target/site/jacoco -->
            </goals>
        </execution>
    </executions>
</plugin>

DeadlineScore такой же, как в статье 1: он здесь, чтобы было что вернуть после поломок. Остальные файлы песочницы не трогай.

// src/main/java/example/testy/score/DeadlineScore.java
package example.testy.score;

import java.time.Duration;
import java.time.Instant;

/**
 * Зачёт по сроку: сколько баллов за задачу, сданную в такое-то время.
 * Учебное правило серии «От красного к зелёному».
 */
public final class DeadlineScore {

    public static final int FULL = 100;                         // вовремя
    public static final int HALF = 50;                          // опоздал, но не больше чем на сутки
    public static final int NONE = 0;                           // опоздал сильнее

    static final Duration GRACE = Duration.ofHours(24);         // сколько можно опоздать за половину

    private DeadlineScore() {                                   // объекты не нужны: только метод
    }

    public static int score(Instant deadline, Instant submittedAt) {
        if (deadline == null) {
            throw new IllegalArgumentException("Срок не указан");
        }
        if (submittedAt == null) {
            throw new IllegalArgumentException("Время сдачи не указано");
        }
        if (!submittedAt.isAfter(deadline)) {                   // ровно в срок — ещё вовремя
            return FULL;
        }
        if (!submittedAt.isAfter(deadline.plus(GRACE))) {       // ровно через сутки — ещё половина
            return HALF;
        }
        return NONE;
    }
}

Два файла для опытов — учебные: так тесты не пишут, это сказано в их первых строках. Клади их в папку src/test/java/example/testy/score/ под именами EmptyTest.java и HollowTest.java, после опытов удали.

// фрагмент: учебный, так НЕ пишут — четыре теста, зелёные при любой поломке
package example.testy.score;

import org.junit.jupiter.api.Test;

import java.time.Duration;
import java.time.Instant;

import static org.assertj.core.api.Assertions.assertThat;

class EmptyTest {

    private static final Instant DEADLINE = Instant.parse("2026-10-12T21:00:00Z");

    @Test
    void noAssertion() {
        DeadlineScore.score(DEADLINE, DEADLINE);                    // вызвали — и всё
    }

    @Test
    void swallowsException() {
        try {
            DeadlineScore.score(null, DEADLINE);
        } catch (IllegalArgumentException expected) {
            // ждали исключение — и молча проглотили; а если его не будет?
        }
    }

    @Test
    void agreesWithItself() {
        Instant hourLate = DEADLINE.plus(Duration.ofHours(1));

        assertThat(DeadlineScore.score(DEADLINE, hourLate))
                .isEqualTo(DeadlineScore.score(DEADLINE, hourLate));   // код сверяется сам с собой
    }

    @Test
    void alwaysTrue() {
        assertThat(DeadlineScore.score(DEADLINE, DEADLINE)).isBetween(0, 100);   // верно при любом правиле
    }
}
// фрагмент: учебный, так НЕ пишут — 100% покрытия и ни одной проверки
package example.testy.score;

import org.junit.jupiter.api.Test;

import java.time.Duration;
import java.time.Instant;

class HollowTest {

    private static final Instant DEADLINE = Instant.parse("2026-10-12T21:00:00Z");

    @Test
    void touchesEveryLine() {
        DeadlineScore.score(DEADLINE, DEADLINE);                            // ветвь «вовремя»
        DeadlineScore.score(DEADLINE, DEADLINE.plus(Duration.ofHours(1)));  // ветвь «половина»
        DeadlineScore.score(DEADLINE, DEADLINE.plus(Duration.ofDays(2)));   // ветвь «ноль»
        try {
            DeadlineScore.score(null, DEADLINE);                            // ветвь «нет срока»
        } catch (IllegalArgumentException ignored) {
        }
        try {
            DeadlineScore.score(DEADLINE, null);                            // ветвь «нет времени сдачи»
        } catch (IllegalArgumentException ignored) {
        }
    }
}

Сделай руками:

  1. Добавь плагин в pom.xml и запусти ./mvnw clean verify: Tests run: 18, всё зелёное. Открой target/site/jacoco/index.html, пакет example.testy.score, класс DeadlineScore: 100% инструкций и ветвей, пропущенных строк — 0 из 10.
  2. Положи оба учебных файла и снова запусти ./mvnw clean verify: Tests run: 23, всё зелёное. Пустые тесты сборку не роняют.
  3. ./mvnw clean verify -Dtest=HollowTest — Tests run: 1, а -Dtest=DeadlineScoreTest — Tests run: 12. У DeadlineScore в обоих отчётах то же, что в первом шаге.
  4. ./mvnw clean verify -Dtest=EmptyTest — Tests run: 4; пропущены 2 строки из 10, ветвей пройдено 75%. Найди две красные строки и два жёлтых ромбика.
  5. Теперь без clean: ./mvnw verify -Dtest=HollowTest, а за ним снова ./mvnw verify -Dtest=EmptyTest. У DeadlineScore опять 0 пропущенных строк: отчёт сложил прогоны. Вот зачем clean.
  6. Мутанты из части 3 — по одному: правка, ./mvnw test, сверка с таблицей, возврат. EmptyTest и HollowTest зелёные на каждом. Колонку статьи 2 так не сверить: тех четырёх тестов в песочнице уже нет, они стали строками таблицы статьи 4.
  7. Удали EmptyTest и HollowTest: так не пишут даже в песочнице. ./mvnw test — снова Tests run: 18. Плагин JaCoCo оставь.

Теперь ты знаешь, чем зелёный тест отличается от проверенного. Первый просто ни разу не краснел, второй краснеет на каждой поломке, которую стережёт.

← Все статьи о тестах

Попробовать руками

В кузницах Hammerhall — задачи по Java, которые проверяет сервер: решаешь в своей IDE, отправляешь одной командой, проверка запускает твои тесты и закрытые. Сложность растёт вместе с решённым, первые задачи бесплатны.