JUnit для нагрузки

Смирнов Вячеслав

Miro

Инженер Miro

Развиваю qa_load

Применим JUnit для профессиональной отладки тестов

Содержание

  1. ⁉️ Чем JUnit полезен нагрузочнику

  2. 🔬 Gatling и его отладка c @Test

  3. 🔬 JMeter-Java-DSL и его отладка c @Test

  4. 📊 Start/Stop-time и их фиксация при отладке

  5. 📊 TestId/RunId/*Id и его генерация при отладке

  6. ⚙️ @ParameterizedTest или параметры как код

  7. ⚙️ @RepeatedTest для тестов масштабируемости

  8. ⚙️ @Execution(ExecutionMode.CONCURRENT) для многопоточности

Чем JUnit полезен нагрузочнику

1. ⁉️ Чем JUnit полезен нагрузочнику

  • Реализация подхода «всё как код»

  • Отладка

  • Параметризация

  • Простые тесты без метрик

Реализация подхода «всё как код»

  • тесты, как код — JMeter-java-dsl, Gatling

  • тестовые данные, как код — @Parametrized

  • параметры отладки, как код — @Test, @IntelliJ IDEA, JUnit Plugin

  • библиотеки и версии, как код — Maven, Laconic POM for Maven

  • презентация по JUnit, как код — Visual Studio Code, Marp.App Extension

Отладка

Сокращение времени на реализацию сложных тестов

Тестирование сложных и новых систем

Рост сложности задач

Карьерный рост

«Всё как код» и отладка

2. 🔬 Gatling и его отладка c @Test

  • Отладка для gatling-java-dsl

  • Отладка для gatling-scala-dsl

Отладка для gatling-java-dsl

    import io.gatling.app.Gatling;
    import org.testng.annotations.Test;
    import scala.collection.mutable.HashMap;
    public class DebugTest {
        var config = new HashMap<String, String>();
        void runSimulation(String simulationClass) {
            config.put("gatling.core.simulationClass", 
                simulationClass);
            Gatling.fromMap(config);
        }
        @Test void debugMaxPerfSimulation() {
            runSimulation("simulation.MaxPerf");
        }
    }

HashMap с настройками и метод запуска симуляции

    import io.gatling.app.Gatling;
    import org.testng.annotations.Test;
    import scala.collection.mutable.HashMap;
    public class DebugTest {
        var config = new HashMap<String, String>();
        void runSimulation(String simulationClass) {
            config.put("gatling.core.simulationClass", 
                simulationClass);
            Gatling.fromMap(config);
        }
        @Test void debugMaxPerfSimulation() {
            runSimulation("simulation.MaxPerf");
        }
    }

@Test, запускающий симуляцию

    import io.gatling.app.Gatling;
    import org.testng.annotations.Test;
    import scala.collection.mutable.HashMap;
    public class DebugTest {
        var config = new HashMap<String, String>();
        void runSimulation(String simulationClass) {
            config.put("gatling.core.simulationClass", 
                simulationClass);
            Gatling.fromMap(config);
        }
        @Test void debugMaxPerfSimulation() {
            runSimulation("simulation.MaxPerf");
        }
    }

Ставим точку останова 🔴 и запускаем отладку 🪲

    import io.gatling.app.Gatling;
    import org.testng.annotations.Test;
    import scala.collection.mutable.HashMap;
    public class DebugTest {
        var config = new HashMap<String, String>();
        void runSimulation(String simulationClass) {
            config.put("gatling.core.simulationClass", 
                simulationClass);
            Gatling.fromMap(config);
        }
🪲      @Test void debugMaxPerfSimulation() {
🔴          runSimulation("simulation.MaxPerf");
        }
    }

Запускаем разные симуляции через @Test

    @Test void maxPerfSimulation() {
        runSimulation("simulation.MaxPerf");
    }

    @Test void stableSimulation() {
        runSimulation("simulation.Stable");
    }

    @Test void onceOnlySimulation() {
        runSimulation("simulation.OnceOnly");
    }

Генерируем html-отчеты через @Test

    @Test void generateReport() {
        HashMap<String, String> configLocal = new HashMap<>();
        {
            configLocal.put("gatling.charting.maxPlotPerSeries", 
            "600");
            configLocal.put("gatling.core.directory.reportsOnly", 
            "gatling/maxperfsimulation-20220321094726824");
        }
        Gatling.fromMap(configLocal);
    }

JUnit удобен для отладки, запуска, ... Gatling Java-DSL

Отладка для gatling-scala-dsl

    import io.gatling.app.Gatling
    import io.gatling.core.ConfigKeys.{core, data}

    object DebugApp extends App {

      val config = scala.collection.mutable.Map(
        core.SimulationClass ->  "simulation.MaxPerf"  
        )

      Gatling.fromMap(config)
    }

Ставим точку останова 🔴 и запускаем отладку 🪲

    import io.gatling.app.Gatling
    import io.gatling.core.ConfigKeys.{core, data}

🪲  object DebugApp extends App {

      val config = scala.collection.mutable.Map(
        core.SimulationClass ->  "simulation.MaxPerf"  
        )

🔴    Gatling.fromMap(config)
    }

Несколько симуляций в одном классе c @Test

    import io.gatling.app.Gatling
    import io.gatling.core.ConfigKeys.{core, data}
    import org.junit.Test
    class DebugTest {
      var config = scala.collection.mutable.Map()
🪲    @Test def maxPerfSimulation() {
        config.put(core.SimulationClass, "simulation.MaxPerf")
        Gatling.fromMap(config);
      }
🪲    @Test def stableSimulation() {
        config.put(core.SimulationClass, "simulation.Stable")
        Gatling.fromMap(config);
      }
    }

JUnit удобен для отладки, запуска, ... Gatling Scala-DSL

3. 🔬 JMeter-Java-DSL и его отладка c @Test

  • Отладка для jmeter-java-dsl

  • Отладка для lambda-блоков jsr223Sampler(v -> {...})

Отладка для jmeter-java-dsl (by desing)

    public class ZeroTest {
        @Test public void zero() throws IOException {
            TestPlanStats stats = testPlan(
                threadGroup(1, 1,
                    jsr223Sampler("zero", v -> {
                        v.log.info("Hello World!");
                    })
                )
            ).run();
            assertThat(stats.overall().sampleTimePercentile99())
                .isLessThan(Duration.ofSeconds(5));
        }
    }

Ставим точку останова 🔴 и запускаем отладку 🪲

    public class ZeroTest {
🪲      @Test public void zero() throws IOException {
🔴          TestPlanStats stats = testPlan(
🔴              threadGroup(1, 1,
🔴                  jsr223Sampler("zero", v -> {
                        v.log.info("Hello World!");
🔴                  })
🔴              )
🔴          ).run();
🔴          assertThat(stats.overall().sampleTimePercentile99())
🔴              .isLessThan(Duration.ofSeconds(5));
        }
    }

Lambda-выражения отлаживать нельзя 💔

    public class ZeroTest {
🪲      @Test public void zero() throws IOException {
            TestPlanStats stats = testPlan(
                threadGroup(1, 1,
                    jsr223Sampler("zero", v -> {
💔                      v.log.info("Hello World!");
                    })
                )
            ).run();
            assertThat(stats.overall().sampleTimePercentile99())
                .isLessThan(Duration.ofSeconds(5));
        }
    }

Но можно создать вспомогательные классы 😎

Создать в них конструкторы и ссылки на все нужное

    public class AbstractScenario {
        public AbstractScenario AbstractScenario(
            DslJsr223Sampler.SamplerVars v) {
            this.samplerVars = v;
            return this.setLog(v.log)
                    .setCtx(v.ctx)
                    .setVars(v.vars)
                    .setProps(v.props)
                    .setSampleResult(v.sampleResult);
        }
        ... 
    }

И писать методы классов

    public class SomeScenario extends AbstractScenario {
        int errorCount = 0;
        public boolean helloWorld() {
            this.log.info("Hello World!");
        }
    }

Легко отлаживаемые методы классов

    public class SomeScenario extends AbstractScenario {
        int errorCount = 0;
        public boolean helloWorld() {
🔴          this.log.info("Hello World!");
        }
    }

Которые вызывать в lambda-выражениях

    public class SomeScenario extends AbstractScenario {
        int errorCount = 0;
        public boolean helloWorld() {
🔴          this.log.info("Hello World!");
⬆      }
⬆  }
    public class ZeroTest {
🪲      @Test public void zero() throws IOException {
            TestPlanStats stats = testPlan(
⬆              threadGroup(1, 1,
⬆                  jsr223Sampler("zero", v -> {
⬆  ⬅  ⬅  ⬅  ⬅      new SomeScenario(v).helloWorld();
                    }))).run();
        }   
    }

JUnit удобен для отладки, запуска, ... JMeter Java-DSL

TestId/RunId/*Id или Start+StopTime для сравнения тестов

4. 📊 Start/Stop-time и их фиксация при отладке

  • Grafana и сравнение метрик из Prometheus, Victoria, InfluxDB

  • Jenkins и фиксация моментов старта и завершения теста

  • JUnit для фиксации Start/Stop-time при отладке

Grafana и сравнение метрик Prometheus, InfluxDB, ...

Слайды: https://polarnik.github.io/grafana-comparator/

Grafana и сравнение метрик Prometheus, InfluxDB, ...

1. Отобразить таблицу по первому тесту:
  - "Start" и "Stop", тип "tag"/"label", формат unixTimeStamp
2. При клике по строке первой таблицы сохраняем в URL: 
  - from=Start, to=Stop, Start1=Start
3. Отобразить таблицу по второму тесту
  - "Start" и "Stop", тип "tag"/"label", формат unixTimeStamp
  - Start1 - дополнительная колонка из переменной Start1
  - Offset - вычисляемая колонка Start1-Start
4. При клике по строке второй таблицы сохраняем в URL: 
  - Offset=Offset
5. Отобразить метрики по первому и второму тестам
  - по первому просто от from до to
  - по второму со смещением Offset

Как сохранить Start/Stop-time для каождого запуска теста?

Jenkins и фиксация моментов старта и завершения

До теста: сохранить Start и расчётный Stop-time

  • как tag/label + дополнительные теги

Тест: ab, curl+bash, k6, jmeter, locust, gatling, junit, ...

  • любой тест!

После теста: сохранить Start и Stop-time фактические

  • как tag/label + дополнительные теги

Пример записи метрик в формате InfluxLine в VM

    metricTime = 1640980800 # Fri Dec 31 2021 20:00:00 GMT+0000
    reqBody = f'testStats,' \
            f'suite={row["suite"]},' \
            f'environment={row["env"]},' \
            f'version={row["version"]},' \
            f'start={row["startUnix"]},' \
            f'stop={row["stopUnix"]} ' \
            f'duration={row["duration"]} {metricTime}000000000'

    http.post(
        url="http://victoriaMetrics:8428/write",
        data=reqBody
    )

@BeforeAll, @AfterAll из JUnit 5

@BeforeClass, @AfterClass из JUnit 4

    @BeforeClass public static void storeStartStopTimeBeforeTest()      {
        var suite = "debug-max-perf";
        var version = "1.111111";
        long start = Instant.now().getEpochSecond();
        long duration = 60 * 60 * 1000; // 1 hour
        long stop = start + duration;
        var metricTime = "1640980800" + "000000000";
        var reqBody = String.format("testStats," +
                "suite=%s,environment=%s,version=%s," +
                "start=%d,stop=%d duration=%d %s",
                suite, env, version, start, stop, 
                duration, metricTime); ...
    }

Можно не сохранять duration

    // testStats_duration:
    var reqBody = String.format("testStats," +
            "suite=%s,environment=%s,version=%s," +
            "start=%d,stop=%d duration=%d %s",
            suite, env, version, startUnix, stopUnix, duration, metricTime);

    // testStats_stop:
    var reqBody = String.format("testStats," +
            "suite=%s,environment=%s,version=%s," +
            "start=%d stop=%d %s",
            suite, env, version, startUnix, stopUnix, metricTime);

Главное -- сохранить start и stop

JUnit удобен для фиксации Start/Stop-time при отладке без CI

5. 📊 TestId/RunId/*Id и его генерация при отладке

  • InfluxDB или Prometheus для аггрегации результатов теста

  • Jenkins и результаты отдельного теста

  • JUnit для генерации TestId/RunId/*Id при отладке

Хочется аггрегировать метрики по тесту

Удобный ключ группировки -- BUILD_ID

https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#working-with-your-jenkinsfile

Имитация BUILD_ID для тестов с @BeforeEach

    static String BUILD_ID;
    @BeforeEach public void beforeEach()
    {
        BUILD_ID = Instant.now().toString().replace(":", "-");
    }
    @Test public void test() throws IOException {
        TestPlanStats stats = testPlan(
            threadGroup(1, 1, jsr223Sampler("test", v -> {
                    v.log.info("Hello World!"); })
            ),
            influxDbListener("http://influxdb:8086/write?db=jmeter")    
                .tag("BUILD_ID", BUILD_ID),
        ).run();
    }

JUnit удобен для эмуляции TestId/RunId/*Id при отладке без CI

Параметры теста как код

Проводил серию тестов с разным ростом нагрузки

6. ⚙️ @ParameterizedTest или параметры как код

    @ParameterizedTest
    @CsvSource({
            "slow,   100, 10",
            "medium, 100,  5",
            "fast,   100,  2",
            "ultra,  100,  1",
            "wow,    100,  0"
    })
    public void loadTest(String testName,
                         int threads,
                         int rampDurationMinutes)
        throws IOException, InterruptedException, TimeoutException {    
        ...
    }

Ступени сложны в анализе, требуют инженера

Тесты-ступени просты в анализе

Assertion на каждую ступень можно задать кодом

    @ParameterizedTest
    @CsvSource({
            "100",
            "200",
            "300",
            "400",    
    })
    public void loadTest(int threads) {
        TestPlanStats stats = testPlan( ... ).run();

        assertThat(stats.overall().sampleTimePercentile99()).
            isLessThan(Duration.ofSeconds(30));
    }

JUnit удобен для простых в анализе тестов с параметрами

Надежные и простые тесты на циклах и потоках

7. ⚙️ @RepeatedTest для тестов масштабируемости

    @Execution(ExecutionMode.SAME_THREAD)
    public class ALotOfLiteMember {
        static LinkedBlockingQueue<HazelcastInstance> queueHz = 
            new LinkedBlockingQueue<>();
        static LinkedBlockingQueue<Integer> queueStop = 
            new LinkedBlockingQueue<>();
        @RepeatedTest(40) public void ConnectToHazelcast() 
            throws InterruptedException {
            var config = new ConfigBuilder().buildConfig();
            var hazelcastInstance = 
                Hazelcast.newHazelcastInstance(config);
            queueHz.put(hazelcastInstance);
            queueStop.poll(30, TimeUnit.SECONDS);
        }
    }

40 итераций с паузами по 30 секунд между ними

    @Execution(ExecutionMode.SAME_THREAD)
    public class ALotOfLiteMember {
        static LinkedBlockingQueue<HazelcastInstance> queueHz = 
            new LinkedBlockingQueue<>();
        static LinkedBlockingQueue<Integer> queueStop = 
            new LinkedBlockingQueue<>();
➡      @RepeatedTest(40) public void ConnectToHazelcast() 
            throws InterruptedException {
            var config = new ConfigBuilder().buildConfig();
            var hazelcastInstance = 
                Hazelcast.newHazelcastInstance(config);    
            queueHz.put(hazelcastInstance);
➡          queueStop.poll(30, TimeUnit.SECONDS);
        }
    }

8. ⚙️ @Execution(ExecutionMode.CONCURRENT)

    @Execution(ExecutionMode.CONCURRENT)
    public class ClusterBench {
        static Cluster cluster = new ClusterImpl();

        @RepeatedTest(30)
        public void getOptionalParam() {
            int loop = 10000 * 2;
            for (int i = 0; i < loop; i++) {
                cluster.getOptionalParam(i);
    }   }   }

Можно тестировать в несколько потоков

JUnit удобен для простых тестов с итерациями и паузами

Начни свой день с кофе

Начни свой тест с JUnit !

JUnit для нагрузки: «Начни свой тест с JUnit

  1. ⁉️ Чем JUnit полезен нагрузочнику
  2. 🔬 Gatling и его отладка c @Test
  3. 🔬 JMeter-Java-DSL и его отладка c @Test
  4. 📊 Start/Stop-time и их фиксация при отладке
  5. 📊 TestId/RunId/*Id и его генерация при отладке
  6. ⚙️ @ParameterizedTest или параметры как код
  7. ⚙️ @RepeatedTest для тестов масштабируемости
  8. ⚙️ @Execution(ExecutionMode.CONCURRENT) для многопоточности

Смирнов Вячеслав | Miro, qa_load, smirnovqa

🔗 polarnik.github.io/junit-for-load-testing/

Повышаю качество более десяти лет. Занимаюсь системой дистанционного банковского обслуживания юридических лиц. Основной профиль моей работы — тестирование производительности. Развиваю сообщество инженеров по тестированию производительности, помогая коллегам в telegram чате «QA — Load & Performance».