Cover
PokeAPI, k6

Advanced k6 Techniques: Optimising Performance Testing for PokeAPI

Introduction
Advanced k6 Techniques: Optimising Performance Testing for PokeAPI

Welcome back to my blog series on using k6 to test the performance of PokeAPI! Part 5! Even I'm a little surprised I've kept this up, to be honest. Anyway, in part 5 I'll show some more advanced k6 features and techniques which can help optimise your performance testing efforts. Covering topics such as custom metrics, threshold analysis, and test script modularization.

Using custom metrics to measure application-specific performance indicators

Custom metrics in k6 allow you to measure and track application-specific performance indicators which are not covered by the built-in metrics.

They can help you gain a more profound insight into your API's performance and identify areas for improvement. k6 supports four types of custom metrics: Counter, Gauge, Rate, and Trend.

Here's an example of how to use custom metrics to track the total number of Pokémon with a specific ability:

import http from 'k6/http';
import { check, sleep } from 'k6';
import { Counter } from 'k6/metrics';

const pokemonWithAbility = new Counter('pokemon_with_ability');

export let options = {
  vus: 10,
  duration: '30s',
};

export default function () {
  const response = http.get('https://pokeapi.co/api/v2/pokemon');
  const jsonData = JSON.parse(response.body);

  jsonData.results.forEach((pokemon) => {
    const pokemonResponse = http.get(pokemon.url);
    const pokemonData = JSON.parse(pokemonResponse.body);

    if (pokemonData.abilities.some((ability) => ability.ability.name === 'chlorophyll')) {
      pokemonWithAbility.add(1);
    }
  });

  sleep(1);
}
I've created a custom Counter metric called pokemon_with_ability to track the total number of Pokémon with the ability 'chlorophyll'.

Setting performance thresholds for pass/fail criteria

Thresholds in k6 allow you to define pass and fail criteria for your performance tests based on specific metrics or custom metrics.

By setting thresholds, you can ensure that your API meets the required performance standards and automatically fail the test if it doesn't.

Here's an example of how to set thresholds for response times and the custom metric created above:

import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate } from 'k6/metrics';

const failedRequests = new Rate('failed_requests');

export let options = {
  vus: 10,
  duration: '30s',
  thresholds: {
    http_req_duration: ['p(95)<300'], // 95% of requests should have a response time below 300ms
    failed_requests: ['rate<0.1'], // failed requests should be less than 10%
  },
};

export default function () {
  const response = http.get('https://pokeapi.co/api/v2/pokemon');

  check(response, {
    'status is 200': (r) => r.status === 200,
  }) || failedRequests.add(1);

  sleep(1);
}

Here I have set two thresholds:

  1. 95% of requests should have a response time below 300 milliseconds.
  2. The rate of failed requests (i.e., requests with a non-200 status code) should be less than 10%.

Modularising test scripts for maintainability and reusability

Keeping your test scripts organised, maintainable, and reusable will be essential as they grow in complexity. The best way, I have found, is to modularise your test scripts by breaking them into smaller, more focused components that can be imported and used as needed.

Here's an example of how to create a modular test script for the PokeAPI:

First things first, create a pokeapi.js file containing the API functions:

import http from 'k6/http';

export function getPokemonList() {
  return http.get('https://pokeapi.co/api/v2/pokemon');
}

export function getPokemonDetails(pokemonId) {
  return http.get(`https://pokeapi.co/api/v2/pokemon/${pokemonId}`);
}

export function getAbilityList() {
  return http.get('https://pokeapi.co/api/v2/ability');
}

Then, create a main-test.js file that imports and uses the modularised API functions:

import { check } from 'k6';
import * as pokeapi from './pokeapi.js';

export let options = {
  vus: 10,
  duration: '30s',
};

export default function () {
  const pokemonListResponse = pokeapi.getPokemonList();
  check(pokemonListResponse, {
    'status is 200 for pokemon list': (r) => r.status === 200,
  });

  const pokemonDetailsResponse = pokeapi.getPokemonDetails(1);
  check(pokemonDetailsResponse, {
    'status is 200 for pokemon details': (r) => r.status === 200,
  });

  const abilityListResponse = pokeapi.getAbilityList();
  check(abilityListResponse, {
    'status is 200 for ability list': (r) => r.status === 200,
  });
}

By modularising the test script, you separate the API functions from the main test logic, making it easier to maintain and reuse these components in other tests.

Hopefully, this introduction into more advanced k6 features and techniques, such as custom metrics, threshold analysis, and test script modularization, to optimise PokeAPI performance testing has been useful. Stay tuned for the next instalment in my blog series, where I'll discuss integrating k6 tests into a Continuous Integration (CI) pipeline to automate and streamline your testing efforts.

Lewys
Author

Lewys

Experienced tester at a mission-critical communications company. With a focus on performance and non-functional testing, I share insights to help myself and fellow testers enhance our skills.

View Comments
Next Post

Transforming User Stories into Performance Wins

Previous Post

Implementing Assertions and Error Handling: Enhancing k6 Performance Testing for PokeAPI