|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +title: Czy wiesz, czym jest narzędzie artillery? |
| 4 | +description: "" |
| 5 | +date: 2026-02-13T08:00:00+01:00 |
| 6 | +published: true |
| 7 | +didyouknow: false |
| 8 | +lang: pl |
| 9 | +author: kdudek |
| 10 | +image: /assets/img/posts/2026-02-13-czy-wiesz-czym-jest-narzedzie-artillery/thumbnail.webp |
| 11 | +tags: |
| 12 | +- artillery |
| 13 | +- tests |
| 14 | +--- |
| 15 | + |
| 16 | +Artillery to oparte o Node.js narzędzie do wykonywania testów wydajnościowych, które może być prostszą alternatywą dla np. Gatlinga. |
| 17 | +Charakteryzuje się ono prostotą użycia, wspiera różne technologie (np. HTTP API, WebSockety), daje możliwość testowania rozproszonego oraz |
| 18 | +może być rozszerzane przez pluginy. |
| 19 | + |
| 20 | +## Instalacja |
| 21 | +Artillery można zainstalować poprzez: |
| 22 | + |
| 23 | +```shell |
| 24 | +npm install -g artillery@latest |
| 25 | +``` |
| 26 | + |
| 27 | +lub używając odpowiedniego obrazu Dockerowego. Same testy pisane są w YAMLu lub w JavaScripcie. |
| 28 | + |
| 29 | +## Przykładowy test |
| 30 | + |
| 31 | +Załóżmy, że nasza aplikacja wystawia trzy usługi: |
| 32 | + |
| 33 | +- `GET /get-projects` – zwracająca listę projektów |
| 34 | +- `GET /get-sprints?project={{nazwa_projektu}}` – zwracająca listę trwających sprintów dla danego projektu |
| 35 | +- `POST /add-task` – pozwalająca na dodanie zadania do sprintu |
| 36 | + |
| 37 | +Chcemy przetestować dwa scenariusze: |
| 38 | + |
| 39 | +1. Użytkownik wchodzi na listę projektów, wyświetla listę sprintów dla projektu TEST-PROJECT, a następnie po dwóch sekundach dodaje zadanie do pierwszego sprintu z listy. |
| 40 | +2. Użytkownik dodaje po kolei 10 zadań do losowych dostępnych sprintów. |
| 41 | + |
| 42 | +Dodatkowo zakładamy trzy fazy: |
| 43 | +- pierwsza, rozgrzewkowa - 5 wirtualnych użytkowników na sekundę, zwiększających się do 10 na koniec fazy, |
| 44 | +```yaml |
| 45 | + duration: 60 |
| 46 | + arrivalRate: 5 |
| 47 | + rampTo: 10 |
| 48 | + name: Warm up phase |
| 49 | +``` |
| 50 | +- druga, stopniowo zwiększająca obciążenie do 50 użytkowników, |
| 51 | +```yaml |
| 52 | + duration: 60 |
| 53 | + arrivalRate: 10 |
| 54 | + rampTo: 50 |
| 55 | + name: Ramp up load |
| 56 | +``` |
| 57 | +
|
| 58 | +- trzecia testująca duży przypływ użytkowników - 50 użytkowników co sekundę. |
| 59 | +```yaml |
| 60 | + duration: 30 |
| 61 | + arrivalRate: 50 |
| 62 | + name: Spike phase |
| 63 | +``` |
| 64 | +
|
| 65 | +## Konfiguracja testu |
| 66 | +
|
| 67 | +W sekcji `config` tworzymy podstawową konfigurację testu – definiujemy, na jaki adres będą kierowane żądania, fazy testu oraz zmienne, |
| 68 | +które możemy wykorzystać w ramach scenariuszy. |
| 69 | +Dodatkowo zdefiniowany jest plugin rozszerzający wyniki oraz procesor – czyli plik zawierający funkcje JavaScript, które mogą być wykorzystane w teście. |
| 70 | + |
| 71 | +```yaml |
| 72 | +config: |
| 73 | + target: http://localhost:8080 |
| 74 | + phases: [...] |
| 75 | + plugins: |
| 76 | + metrics-by-endpoint: {} |
| 77 | + processor: "./functions.js" |
| 78 | + variables: |
| 79 | + project: "TEST-PROJECT" |
| 80 | +``` |
| 81 | + |
| 82 | +## Scenariusze i flow |
| 83 | + |
| 84 | +Sekcja `scenarios` zawiera definicje scenariuszy testowych. Każdy scenariusz ma określony `weight`, który decyduje o tym, jak często będzie wybierany przez wirtualnych użytkowników. W ramach `flow` określone są kroki scenariusza, w których możemy wykorzystywać zmienne zdefiniowane w konfiguracji lub tworzyć je na bieżąco, np. na podstawie odpowiedzi usług. |
| 85 | + |
| 86 | +```yaml |
| 87 | +scenarios: |
| 88 | + # 9 na 10 użytkowników wybierze ten scenariusz |
| 89 | + - name: "Standard scenario - add single task" |
| 90 | + weight: 9 |
| 91 | + flow: |
| 92 | + - get: |
| 93 | + url: "/get-projects" |
| 94 | + - get: |
| 95 | + url: "/get-sprints" |
| 96 | + qs: |
| 97 | + project: "{{project}}" |
| 98 | + capture: |
| 99 | + json: "$.sprints[0].sprintId" |
| 100 | + as: "sprintId" |
| 101 | + - think: 2 |
| 102 | + - post: |
| 103 | + url: '/add-task' |
| 104 | + json: |
| 105 | + name: "Example task" |
| 106 | + type: "TECHNICAL" |
| 107 | + sprintId: "{{sprintId}}" |
| 108 | + # 1 na 10 użytkowników wybierze ten scenariusz |
| 109 | + - name: "Rare scenario - add multiple tasks" |
| 110 | + weight: 1 |
| 111 | + flow: |
| 112 | + - get: |
| 113 | + url: "/get-projects" |
| 114 | + - get: |
| 115 | + url: "/get-sprints" |
| 116 | + qs: |
| 117 | + project: "{{project}}" |
| 118 | + capture: |
| 119 | + json: "$.sprints" |
| 120 | + as: "sprints" |
| 121 | + - think: 2 |
| 122 | + - loop: |
| 123 | + - post: |
| 124 | + beforeRequest: "setAddTaskBody" |
| 125 | + url: '/add-task' |
| 126 | + count: 10 |
| 127 | +``` |
| 128 | + |
| 129 | +## Funkcje pomocnicze (processor) |
| 130 | + |
| 131 | +Plik `functions.js` z funkcjami pomocniczymi: |
| 132 | + |
| 133 | +```javascript |
| 134 | +module.exports = { |
| 135 | + setAddTaskBody |
| 136 | +} |
| 137 | +
|
| 138 | +function setAddTaskBody(requestParams, context, ee, next) { |
| 139 | + const type = Math.random() < 0.75 ? "BUG" : "TECHNICAL" |
| 140 | + const name = randomString(); |
| 141 | + const sprints = context.vars["sprints"]; |
| 142 | + const randomSprint = sprints[Math.floor(Math.random() * sprints.length)]; |
| 143 | +
|
| 144 | + const task = { |
| 145 | + name, |
| 146 | + type, |
| 147 | + sprintId: randomSprint.sprintId |
| 148 | + } |
| 149 | + requestParams.json = task; |
| 150 | + return next(); |
| 151 | +} |
| 152 | +
|
| 153 | +function randomString() { |
| 154 | + return (Math.random() + 1).toString(36).substring(7); |
| 155 | +} |
| 156 | +``` |
| 157 | + |
| 158 | +## Hooki w Artillery |
| 159 | + |
| 160 | +Artillery pozwala na wykorzystywanie hooków, które są funkcjami JavaScriptowymi zawartymi w pliku będącym procesorem. Wyróżniamy następujące hooki: |
| 161 | + |
| 162 | +- `beforeScenario` i `afterScenario` – przed/po wykonaniu scenariusza przez wirtualnego użytkownika |
| 163 | +- `beforeRequest` – przed wysłaniem requestu (można ustawić parametry takie jak URL, ciasteczka, nagłówki czy ciało żądania) |
| 164 | +- `afterResponse` – po otrzymaniu odpowiedzi (np. zdefiniować zmienne na dalsze potrzeby testu) |
| 165 | +- `function` – funkcja wywoływana w dowolnym miejscu scenariusza |
| 166 | + |
| 167 | +W powyższej implementacji scenariusza wykorzystany został `beforeRequest`, będący funkcją w której ustawiamy body żądania na podstawie wcześniej zapisanej zmiennej. |
| 168 | +Funkcja ta przyjmuje cztery parametry: |
| 169 | + |
| 170 | +- `requestParams` – obiekt żądania |
| 171 | +- `context` – kontekst wirtualnego użytkownika; za pośrednictwem `context.vars` mamy dostęp do wszystkich zdefiniowanych zmiennych |
| 172 | +- `ee` – event emitter do bezpośredniej komunikacji z Artillery |
| 173 | +- `next` – obowiązkowy callback, dzięki któremu test jest kontynuowany |
| 174 | + |
| 175 | +## Uruchamianie testów |
| 176 | + |
| 177 | +Aby uruchomić taki scenariusz wystarczy użyć komendy: |
| 178 | + |
| 179 | +```shell |
| 180 | +artillery run scenario.yml |
| 181 | +``` |
| 182 | + |
| 183 | +Można zapisać wyniki testu do formatu JSON dodając flagę `--output`: |
| 184 | + |
| 185 | +```shell |
| 186 | +artillery run --output results.json scenario.yml |
| 187 | +``` |
| 188 | + |
| 189 | +## Raportowanie wyników |
| 190 | + |
| 191 | +Plik ten można wykorzystać do wygenerowania raportu HTML za pośrednictwem komendy: |
| 192 | + |
| 193 | +```shell |
| 194 | +artillery report results.json |
| 195 | +``` |
| 196 | + |
| 197 | +Fragment wygenerowanego raportu, przedstawiający czasy odpowiedzi (ich minimalną i maksymalną wartość, a także medianę oraz percentyle 95 i 99) dla wszystkich żądań HTTP wysłanych w ramach scenariuszy: |
| 198 | + |
| 199 | +<img src="/assets/img/posts/2026-02-13-czy-wiesz-czym-jest-narzedzie-artillery/http_response_time.png" alt="Fragment raportu wygenerowanego przez Artillery" /> |
| 200 | + |
| 201 | +## Podsumowanie |
| 202 | + |
| 203 | +Poniżej pełny przykład pliku testowego YAML oraz procesora JS, zbierający wszystkie elementy opisane powyżej: |
| 204 | + |
| 205 | +```yaml |
| 206 | +config: |
| 207 | + target: http://localhost:8080 |
| 208 | + phases: |
| 209 | + - duration: 60 |
| 210 | + arrivalRate: 5 |
| 211 | + rampTo: 10 |
| 212 | + name: Warm up phase |
| 213 | + - duration: 60 |
| 214 | + arrivalRate: 10 |
| 215 | + rampTo: 50 |
| 216 | + name: Ramp up load |
| 217 | + - duration: 30 |
| 218 | + arrivalRate: 50 |
| 219 | + name: Spike phase |
| 220 | + plugins: |
| 221 | + metrics-by-endpoint: {} |
| 222 | + processor: "./functions.js" |
| 223 | + variables: |
| 224 | + project: "TEST-PROJECT" |
| 225 | +scenarios: |
| 226 | + - name: "Standard scenario - add single task" |
| 227 | + weight: 9 |
| 228 | + flow: |
| 229 | + - get: |
| 230 | + url: "/get-projects" |
| 231 | + - get: |
| 232 | + url: "/get-sprints" |
| 233 | + qs: |
| 234 | + project: "{{project}}" |
| 235 | + capture: |
| 236 | + json: "$.sprints[0].sprintId" |
| 237 | + as: "sprintId" |
| 238 | + - think: 2 |
| 239 | + - post: |
| 240 | + url: '/add-task' |
| 241 | + json: |
| 242 | + name: "Example task" |
| 243 | + type: "TECHNICAL" |
| 244 | + sprintId: "{{sprintId}}" |
| 245 | + - name: "Rare scenario - add multiple tasks" |
| 246 | + weight: 1 |
| 247 | + flow: |
| 248 | + - get: |
| 249 | + url: "/get-projects" |
| 250 | + - get: |
| 251 | + url: "/get-sprints" |
| 252 | + qs: |
| 253 | + project: "{{project}}" |
| 254 | + capture: |
| 255 | + json: "$.sprints" |
| 256 | + as: "sprints" |
| 257 | + - think: 2 |
| 258 | + - loop: |
| 259 | + - post: |
| 260 | + beforeRequest: "setAddTaskBody" |
| 261 | + url: '/add-task' |
| 262 | + count: 10 |
| 263 | +``` |
| 264 | + |
| 265 | +```javascript |
| 266 | +module.exports = { |
| 267 | + setAddTaskBody |
| 268 | +} |
| 269 | +
|
| 270 | +function setAddTaskBody(requestParams, context, ee, next) { |
| 271 | + const type = Math.random() < 0.75 ? "BUG" : "TECHNICAL" |
| 272 | + const name = randomString(); |
| 273 | + const sprints = context.vars["sprints"]; |
| 274 | + const randomSprint = sprints[Math.floor(Math.random() * sprints.length)]; |
| 275 | +
|
| 276 | + const task = { |
| 277 | + name, |
| 278 | + type, |
| 279 | + sprintId: randomSprint.sprintId |
| 280 | + } |
| 281 | + requestParams.json = task; |
| 282 | + return next(); |
| 283 | +} |
| 284 | +
|
| 285 | +function randomString() { |
| 286 | + return (Math.random() + 1).toString(36).substring(7); |
| 287 | +} |
| 288 | +``` |
| 289 | + |
| 290 | +Artillery to proste i przyjemne narzędzie, które posiada także wiele innych funkcjonalności niewykorzystanych w powyższym przykładzie – można się z nimi zapoznać w dokumentacji. |
| 291 | +Potencjalnym problemem może być jednak brak wsparcia dla równoległych requestów wywoływanych przez jednego wirtualnego użytkownika – chociaż teoretycznie w kodzie Artillery znaleźć można opcję `parallel`, |
| 292 | +to w praktyce nie do końca ona działa. W takiej sytuacji obejściem może być np. odpowiednie zdefiniowanie dodatkowych scenariuszy, które zasymulują takie współbieżne żądania. |
| 293 | + |
| 294 | +## Dokumentacja |
| 295 | +- [Artillery – oficjalna dokumentacja](https://www.artillery.io/docs) |
0 commit comments