Fundamental K6 Load Testing - Performance Testing, Stress Testing, dan Membangun Real-World API Scenarios

Fundamental K6 Load Testing - Performance Testing, Stress Testing, dan Membangun Real-World API Scenarios

Kuasai K6 dari konsep inti hingga produksi. Pelajari load testing, stress testing, dan spike testing. Bangun complete e-commerce API dengan NestJS dan stress test dengan realistic scenarios termasuk checkout flows, inventory management, dan payment processing.

AI Agent
AI AgentFebruary 25, 2026
0 views
13 min read

Pengenalan

Setiap aplikasi memiliki limits. Pada beberapa point, terlalu banyak users hit API Anda simultaneously, dan semuanya breaks. Performance testing mengungkap limits ini sebelum users discover mereka di production.

K6 adalah modern load testing platform yang membuat performance testing accessible, scriptable, dan developer-friendly. Digunakan oleh companies seperti Grafana, Datadog, dan thousands dari engineering teams, K6 enable Anda untuk simulate real-world traffic patterns dan identify bottlenecks sebelum mereka impact users.

Dalam artikel ini, kita akan mengeksplorasi arsitektur K6, memahami load testing fundamentals, dan membangun production-ready e-commerce API dengan NestJS yang kita stress test dengan realistic scenarios termasuk checkout flows, inventory management, dan payment processing.

Mengapa K6 Ada

Masalah Performance Testing

Traditional load testing tools memiliki significant limitations:

Complex Setup: Tools seperti JMeter memerlukan GUI configuration dan sulit untuk version control.

Not Developer-Friendly: Non-developers tidak bisa easily write atau modify tests.

Limited Scripting: Sulit untuk simulate complex user behaviors dan workflows.

Poor Integration: Difficult untuk integrate ke CI/CD pipelines.

Expensive: Enterprise tools cost thousands per month.

Slow Feedback: Results memerlukan hours untuk analyze.

Solusi K6

K6 dibangun oleh Grafana Labs untuk solve problems ini:

JavaScript-Based: Write tests dalam JavaScript, familiar ke developers.

Version Control: Tests adalah code, stored di Git.

CI/CD Integration: Runs di pipelines, automated performance testing.

Realistic Scenarios: Simulate complex user journeys.

Open Source: Free dan community-driven.

Fast Feedback: Results dalam minutes, bukan hours.

Cloud & Local: Run tests locally atau di cloud.

K6 Core Architecture

Key Concepts

Virtual Users (VUs): Simulated users executing test script Anda.

Iterations: Number dari times setiap VU execute test.

Stages: Ramp-up, steady-state, dan ramp-down phases.

Metrics: Response time, throughput, error rate, custom metrics.

Thresholds: Pass/fail criteria untuk tests Anda.

Groups: Organize related requests untuk better reporting.

Checks: Assertions yang validate responses.

Bagaimana K6 Bekerja

plaintext
Test Script → K6 Engine → Virtual Users → HTTP Requests → Target API → Metrics Collection
  1. K6 reads test script Anda
  2. Spawns virtual users
  3. Setiap VU execute requests
  4. Collects metrics (latency, errors, throughput)
  5. Generates reports

K6 Execution Model

K6 menggunakan unique execution model yang optimized untuk performance:

plaintext
Local Machine → K6 Engine (Go-based) → Virtual Users (Lightweight)

                            HTTP Requests

                            Target API

K6 engine ditulis dalam Go, membuat extremely efficient. Setiap virtual user adalah lightweight, allowing thousands dari concurrent users pada single machine.

K6 Core Concepts & Features

1. Virtual Users (VUs) dan Iterations

VUs adalah simulated users. Setiap VU execute test script Anda independently.

Basic VU Configuration
import http from 'k6/http';
import { sleep } from 'k6';
 
export const options = {
  vus: 10,           // 10 virtual users
  duration: '30s',   // Run untuk 30 seconds
};
 
export default function () {
  http.get('https://api.example.com/users');
  sleep(1);
}

Use Cases:

  1. Load Testing: Simulate normal traffic
  2. Stress Testing: Gradually increase load sampai failure
  3. Spike Testing: Sudden traffic increase
  4. Soak Testing: Sustained load over long periods

2. Stages (Ramp-Up dan Ramp-Down)

Stages define bagaimana VUs scale over time.

Staged Load Testing
export const options = {
  stages: [
    { duration: '2m', target: 100 },   // Ramp up ke 100 VUs
    { duration: '5m', target: 100 },   // Stay di 100 VUs
    { duration: '2m', target: 200 },   // Ramp up ke 200 VUs
    { duration: '5m', target: 200 },   // Stay di 200 VUs
    { duration: '2m', target: 0 },     // Ramp down ke 0 VUs
  ],
};
 
export default function () {
  http.get('https://api.example.com/users');
}

Use Cases:

  1. Realistic Traffic: Simulate gradual user increase
  2. Warm-up: Allow system untuk stabilize
  3. Cool-down: Graceful shutdown
  4. Peak Testing: Test di maximum capacity

3. Checks dan Assertions

Checks validate response correctness tanpa fail test.

Checks dan Assertions
import http from 'k6/http';
import { check } from 'k6';
 
export default function () {
  const response = http.get('https://api.example.com/users');
  
  check(response, {
    'status is 200': (r) => r.status === 200,
    'response time < 500ms': (r) => r.timings.duration < 500,
    'has user data': (r) => r.json('data.length') > 0,
  });
}

Use Cases:

  1. Validate Responses: Ensure correct data
  2. Performance Assertions: Check response times
  3. Business Logic: Verify calculations
  4. Data Integrity: Validate returned data

4. Thresholds

Thresholds define pass/fail criteria untuk test Anda.

Thresholds
export const options = {
  thresholds: {
    http_req_duration: ['p(95)<500', 'p(99)<1000'],  // 95th percentile < 500ms
    http_req_failed: ['rate<0.1'],                    // Error rate < 10%
    http_reqs: ['rate>100'],                          // At least 100 req/s
  },
};
 
export default function () {
  http.get('https://api.example.com/users');
}

Use Cases:

  1. SLA Validation: Ensure SLA compliance
  2. Performance Gates: Prevent regressions
  3. Error Rate Limits: Acceptable failure rate
  4. Throughput Requirements: Minimum requests/second

5. Custom Metrics

Track custom metrics specific ke application Anda.

Custom Metrics
import http from 'k6/http';
import { Counter, Trend, Gauge } from 'k6/metrics';
 
const checkoutTime = new Trend('checkout_duration');
const cartAdditions = new Counter('cart_additions');
const activeUsers = new Gauge('active_users');
 
export default function () {
  const startTime = new Date();
  
  http.post('https://api.example.com/cart/add', { productId: 1 });
  cartAdditions.add(1);
  
  const response = http.post('https://api.example.com/checkout', {
    items: [{ id: 1, quantity: 1 }],
  });
  
  checkoutTime.add(new Date() - startTime);
  activeUsers.add(1);
}

Metric Types:

  1. Counter: Cumulative count
  2. Trend: Track values over time (min, max, avg, percentiles)
  3. Gauge: Current value
  4. Rate: Percentage dari events

6. Groups

Organize related requests untuk better reporting.

Groups
import http from 'k6/http';
import { group } from 'k6';
 
export default function () {
  group('User Authentication', function () {
    http.post('https://api.example.com/login', {
      email: 'user@example.com',
      password: 'password',
    });
  });
 
  group('Product Browsing', function () {
    http.get('https://api.example.com/products');
    http.get('https://api.example.com/products/1');
  });
 
  group('Checkout', function () {
    http.post('https://api.example.com/checkout', {
      items: [{ id: 1, quantity: 1 }],
    });
  });
}

Use Cases:

  1. Organize Tests: Group related requests
  2. Detailed Reporting: See metrics per group
  3. Debugging: Identify slow groups
  4. Business Flows: Map user journeys

7. Scenarios

Define different user behaviors dan traffic patterns.

Scenarios
export const options = {
  scenarios: {
    browsing: {
      executor: 'constant-vus',
      vus: 50,
      duration: '5m',
      exec: 'browsing',
    },
    checkout: {
      executor: 'ramping-vus',
      startVUs: 0,
      stages: [
        { duration: '2m', target: 100 },
        { duration: '5m', target: 100 },
        { duration: '2m', target: 0 },
      ],
      exec: 'checkout',
    },
  },
};
 
export function browsing() {
  http.get('https://api.example.com/products');
}
 
export function checkout() {
  http.post('https://api.example.com/checkout', {});
}

Use Cases:

  1. Multiple User Types: Different behaviors
  2. Concurrent Scenarios: Run simultaneously
  3. Realistic Workflows: Mix dari activities
  4. Weighted Distribution: Control traffic split

8. Data Parameterization

Use external data dalam tests Anda.

Data Parameterization
import http from 'k6/http';
import { check } from 'k6';
import papaparse from 'https://jslib.k6.io/papaparse/5.1.1/index.js';
import { SharedArray } from 'k6/data';
 
const data = new SharedArray('users', function () {
  return papaparse.parse(open('./users.csv')).data;
});
 
export default function () {
  const user = data[Math.floor(Math.random() * data.length)];
  
  const response = http.post('https://api.example.com/login', {
    email: user.email,
    password: user.password,
  });
  
  check(response, {
    'login successful': (r) => r.status === 200,
  });
}

Use Cases:

  1. Realistic Data: Use production-like data
  2. Multiple Users: Test dengan different accounts
  3. Varied Inputs: Different request payloads
  4. Distributed Load: Spread across data

9. Correlation dan Dynamic Data

Extract dan use data dari responses.

Correlation
import http from 'k6/http';
 
export default function () {
  // Get product list
  const productsResponse = http.get('https://api.example.com/products');
  const products = productsResponse.json('data');
  
  // Extract product ID
  const productId = products[0].id;
  
  // Use dalam next request
  const detailsResponse = http.get(
    `https://api.example.com/products/${productId}`
  );
  
  // Extract session token
  const sessionToken = detailsResponse.headers['X-Session-Token'];
  
  // Use dalam subsequent requests
  http.post(
    'https://api.example.com/cart/add',
    { productId },
    {
      headers: { 'X-Session-Token': sessionToken },
    }
  );
}

Use Cases:

  1. Session Management: Extract dan use tokens
  2. Dynamic IDs: Use IDs dari responses
  3. Realistic Flows: Simulate user journeys
  4. State Management: Maintain session state

10. Performance Optimization

K6 provides features untuk efficient testing.

Performance Optimization
import http from 'k6/http';
import { batch } from 'k6/http';
 
export const options = {
  // Connection pooling
  ext: {
    loadimpact: {
      projectID: 3356643,
      name: 'API Load Test',
    },
  },
};
 
export default function () {
  // Batch requests untuk efficiency
  batch([
    ['GET', 'https://api.example.com/users'],
    ['GET', 'https://api.example.com/products'],
    ['GET', 'https://api.example.com/orders'],
  ]);
}

Use Cases:

  1. Batch Requests: Reduce overhead
  2. Connection Pooling: Reuse connections
  3. Resource Efficiency: Reduce memory usage
  4. Faster Tests: Complete tests quicker

Membangun Real-World E-Commerce API dengan NestJS

Sekarang mari kita bangun production-ready e-commerce API yang demonstrate realistic scenarios. Sistem handle:

  • Product catalog dengan inventory
  • Shopping cart management
  • Checkout dan payment processing
  • Order tracking
  • Real-time inventory updates
  • Concurrent user handling

Project Setup

Create NestJS project
npm i -g @nestjs/cli
nest new ecommerce-api
cd ecommerce-api
npm install @nestjs/typeorm typeorm pg class-validator class-transformer

Step 1: Database Configuration

src/database/database.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
 
@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: process.env.DB_HOST || 'localhost',
      port: parseInt(process.env.DB_PORT) || 5432,
      username: process.env.DB_USER || 'postgres',
      password: process.env.DB_PASSWORD || 'password',
      database: process.env.DB_NAME || 'ecommerce',
      entities: [__dirname + '/../**/*.entity{.ts,.js}'],
      synchronize: true,
    }),
  ],
})
export class DatabaseModule {}

Step 2: Define Entities

src/entities/product.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
import { CartItem } from './cart-item.entity';
 
@Entity('products')
export class Product {
  @PrimaryGeneratedColumn()
  id: number;
 
  @Column()
  name: string;
 
  @Column('text')
  description: string;
 
  @Column('decimal', { precision: 10, scale: 2 })
  price: number;
 
  @Column({ default: 0 })
  stock: number;
 
  @Column({ default: 0 })
  reserved: number;
 
  @Column({ default: true })
  isActive: boolean;
 
  @OneToMany(() => CartItem, (item) => item.product)
  cartItems: CartItem[];
 
  @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
  createdAt: Date;
 
  @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
  updatedAt: Date;
}

Step 3: Products Service

src/products/products.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Product } from '../entities/product.entity';
 
@Injectable()
export class ProductsService {
  constructor(
    @InjectRepository(Product)
    private productRepository: Repository<Product>,
  ) {}
 
  async getAllProducts(limit: number = 20, offset: number = 0) {
    return this.productRepository.find({
      where: { isActive: true },
      take: limit,
      skip: offset,
    });
  }
 
  async getProductById(id: number) {
    return this.productRepository.findOne({ where: { id } });
  }
 
  async checkAvailability(productId: number, quantity: number) {
    const product = await this.productRepository.findOne({
      where: { id: productId },
    });
 
    if (!product) return false;
    return product.stock - product.reserved >= quantity;
  }
 
  async reserveStock(productId: number, quantity: number) {
    const product = await this.productRepository.findOne({
      where: { id: productId },
    });
 
    if (!product || product.stock - product.reserved < quantity) {
      throw new Error('Insufficient stock');
    }
 
    product.reserved += quantity;
    return this.productRepository.save(product);
  }
 
  async confirmStock(productId: number, quantity: number) {
    const product = await this.productRepository.findOne({
      where: { id: productId },
    });
 
    if (product) {
      product.stock -= quantity;
      product.reserved = Math.max(0, product.reserved - quantity);
      await this.productRepository.save(product);
    }
  }
}

Step 4: Orders Service

src/orders/orders.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Order } from '../entities/order.entity';
import { ProductsService } from '../products/products.service';
 
@Injectable()
export class OrdersService {
  constructor(
    @InjectRepository(Order)
    private orderRepository: Repository<Order>,
    private productsService: ProductsService,
  ) {}
 
  async createOrder(sessionId: string, items: any[]) {
    if (items.length === 0) {
      throw new Error('Cart is empty');
    }
 
    // Reserve stock untuk semua items
    for (const item of items) {
      await this.productsService.reserveStock(item.productId, item.quantity);
    }
 
    const totalAmount = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
 
    const order = this.orderRepository.create({
      sessionId,
      items,
      totalAmount,
      status: 'pending',
    });
 
    return this.orderRepository.save(order);
  }
 
  async processPayment(orderId: string, paymentDetails: any) {
    const order = await this.orderRepository.findOne({
      where: { id: orderId },
    });
 
    if (!order) throw new Error('Order not found');
 
    // Simulate payment processing
    const paymentId = `PAY-${Date.now()}`;
    const isSuccessful = Math.random() > 0.05; // 95% success rate
 
    order.paymentId = paymentId;
    order.paymentStatus = isSuccessful ? 'completed' : 'failed';
    order.status = isSuccessful ? 'processing' : 'failed';
 
    return this.orderRepository.save(order);
  }
 
  async completeOrder(orderId: string) {
    const order = await this.orderRepository.findOne({
      where: { id: orderId },
    });
 
    if (!order) throw new Error('Order not found');
 
    // Confirm stock deduction
    for (const item of order.items) {
      await this.productsService.confirmStock(item.productId, item.quantity);
    }
 
    order.status = 'completed';
    return this.orderRepository.save(order);
  }
 
  async getOrderStatus(orderId: string) {
    return this.orderRepository.findOne({ where: { id: orderId } });
  }
}

Step 5: API Controllers

src/products/products.controller.ts
import { Controller, Get, Param, Query } from '@nestjs/common';
import { ProductsService } from './products.service';
 
@Controller('products')
export class ProductsController {
  constructor(private readonly productsService: ProductsService) {}
 
  @Get()
  async getAllProducts(
    @Query('limit') limit: number = 20,
    @Query('offset') offset: number = 0,
  ) {
    const products = await this.productsService.getAllProducts(limit, offset);
    return { count: products.length, products };
  }
 
  @Get(':id')
  async getProduct(@Param('id') id: number) {
    const product = await this.productsService.getProductById(id);
    return product;
  }
 
  @Get(':id/availability')
  async checkAvailability(
    @Param('id') id: number,
    @Query('quantity') quantity: number,
  ) {
    const available = await this.productsService.checkAvailability(id, quantity);
    return { productId: id, quantity, available };
  }
}
src/orders/orders.controller.ts
import { Controller, Post, Get, Body, Param, Headers } from '@nestjs/common';
import { OrdersService } from './orders.service';
 
@Controller('orders')
export class OrdersController {
  constructor(private readonly ordersService: OrdersService) {}
 
  @Post('create')
  async createOrder(
    @Headers('x-session-id') sessionId: string,
    @Body() createOrderDto: { items: any[] },
  ) {
    return this.ordersService.createOrder(sessionId, createOrderDto.items);
  }
 
  @Post(':id/payment')
  async processPayment(
    @Param('id') orderId: string,
    @Body() paymentDetails: any,
  ) {
    return this.ordersService.processPayment(orderId, paymentDetails);
  }
 
  @Post(':id/complete')
  async completeOrder(@Param('id') orderId: string) {
    return this.ordersService.completeOrder(orderId);
  }
 
  @Get(':id')
  async getOrderStatus(@Param('id') orderId: string) {
    return this.ordersService.getOrderStatus(orderId);
  }
}

Step 6: Docker Compose Setup

docker-compose.yml
version: '3.8'
 
services:
  postgres:
    image: postgres:15
    ports:
      - '5432:5432'
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
      POSTGRES_DB: ecommerce
    volumes:
      - postgres_data:/var/lib/postgresql/data
 
  api:
    build: .
    ports:
      - '3000:3000'
    environment:
      DB_HOST: postgres
      DB_PORT: 5432
      DB_USER: postgres
      DB_PASSWORD: password
      DB_NAME: ecommerce
    depends_on:
      - postgres
 
volumes:
  postgres_data:

Step 7: Running the Application

Start services
# Start services
docker-compose up -d
 
# Install dependencies
npm install
 
# Run application
npm run start:dev

Real-World K6 Load Testing Scenarios

Sekarang mari kita create comprehensive K6 tests yang simulate realistic e-commerce scenarios.

Step 1: Realistic User Journey Test

tests/user-journey-test.js
import http from 'k6/http';
import { check, group, sleep } from 'k6';
 
export const options = {
  stages: [
    { duration: '1m', target: 50 },
    { duration: '3m', target: 100 },
    { duration: '2m', target: 150 },
    { duration: '3m', target: 150 },
    { duration: '2m', target: 0 },
  ],
  thresholds: {
    http_req_duration: ['p(95)<500', 'p(99)<1000'],
    http_req_failed: ['rate<0.05'],
  },
};
 
export default function () {
  const baseUrl = 'http://localhost:3000';
  const sessionId = `session-${__VU}-${Date.now()}`;
 
  group('Browse Products', function () {
    const productsRes = http.get(`${baseUrl}/products?limit=20`, {
      headers: { 'x-session-id': sessionId },
    });
 
    check(productsRes, {
      'browse status 200': (r) => r.status === 200,
      'browse response time < 300ms': (r) => r.timings.duration < 300,
    });
 
    sleep(2);
  });
 
  group('View Product Details', function () {
    const detailRes = http.get(`${baseUrl}/products/1`, {
      headers: { 'x-session-id': sessionId },
    });
 
    check(detailRes, {
      'detail status 200': (r) => r.status === 200,
    });
 
    sleep(1);
  });
 
  group('Checkout', function () {
    const createOrderRes = http.post(
      `${baseUrl}/orders/create`,
      JSON.stringify({
        items: [
          { productId: 1, quantity: 2, price: 100 },
          { productId: 2, quantity: 1, price: 50 },
        ],
      }),
      {
        headers: {
          'x-session-id': sessionId,
          'Content-Type': 'application/json',
        },
      }
    );
 
    check(createOrderRes, {
      'create order status 200': (r) => r.status === 200,
      'order has ID': (r) => r.json('id') !== undefined,
    });
 
    const orderId = createOrderRes.json('id');
    sleep(1);
 
    const paymentRes = http.post(
      `${baseUrl}/orders/${orderId}/payment`,
      JSON.stringify({
        method: 'credit_card',
        amount: createOrderRes.json('totalAmount'),
      }),
      {
        headers: { 'Content-Type': 'application/json' },
      }
    );
 
    check(paymentRes, {
      'payment status 200': (r) => r.status === 200,
    });
 
    sleep(1);
 
    const completeRes = http.post(
      `${baseUrl}/orders/${orderId}/complete`,
      null
    );
 
    check(completeRes, {
      'complete order status 200': (r) => r.status === 200,
      'order completed': (r) => r.json('status') === 'completed',
    });
  });
}

Step 2: Stress Test - Gradual Load Increase

tests/stress-test.js
import http from 'k6/http';
import { check, group } from 'k6';
import { Counter, Trend } from 'k6/metrics';
 
const checkoutErrors = new Counter('checkout_errors');
const checkoutTime = new Trend('checkout_duration');
 
export const options = {
  stages: [
    { duration: '2m', target: 100 },
    { duration: '2m', target: 200 },
    { duration: '2m', target: 300 },
    { duration: '2m', target: 400 },
    { duration: '2m', target: 500 },
    { duration: '3m', target: 0 },
  ],
  thresholds: {
    http_req_failed: ['rate<0.1'],
    checkout_errors: ['count<50'],
    checkout_duration: ['p(95)<2000'],
  },
};
 
export default function () {
  const baseUrl = 'http://localhost:3000';
  const sessionId = `stress-${__VU}-${Date.now()}`;
  const startTime = new Date();
 
  group('Stress Test - Full Checkout Flow', function () {
    // Browse
    http.get(`${baseUrl}/products?limit=20`, {
      headers: { 'x-session-id': sessionId },
    });
 
    // Create order
    const orderRes = http.post(
      `${baseUrl}/orders/create`,
      JSON.stringify({
        items: [
          { productId: Math.floor(Math.random() * 100) + 1, quantity: 1, price: 100 },
        ],
      }),
      {
        headers: {
          'x-session-id': sessionId,
          'Content-Type': 'application/json',
        },
      }
    );
 
    if (orderRes.status !== 200) {
      checkoutErrors.add(1);
    }
 
    const orderId = orderRes.json('id');
 
    // Process payment
    const paymentRes = http.post(
      `${baseUrl}/orders/${orderId}/payment`,
      JSON.stringify({ method: 'credit_card' }),
      { headers: { 'Content-Type': 'application/json' } }
    );
 
    // Complete order
    const completeRes = http.post(`${baseUrl}/orders/${orderId}/complete`, null);
 
    const duration = new Date() - startTime;
    checkoutTime.add(duration);
 
    check(completeRes, {
      'checkout successful': (r) => r.status === 200,
    });
  });
}

Step 3: Spike Test - Sudden Traffic Surge

tests/spike-test.js
import http from 'k6/http';
import { check } from 'k6';
 
export const options = {
  stages: [
    { duration: '1m', target: 50 },      // Normal load
    { duration: '30s', target: 500 },    // Spike ke 500 VUs
    { duration: '1m', target: 500 },     // Maintain spike
    { duration: '30s', target: 50 },     // Back ke normal
    { duration: '1m', target: 0 },       // Cool down
  ],
  thresholds: {
    http_req_duration: ['p(95)<1000'],
    http_req_failed: ['rate<0.2'],
  },
};
 
export default function () {
  const baseUrl = 'http://localhost:3000';
  const sessionId = `spike-${__VU}-${Date.now()}`;
 
  const res = http.get(`${baseUrl}/products?limit=20`, {
    headers: { 'x-session-id': sessionId },
  });
 
  check(res, {
    'spike test status 200': (r) => r.status === 200,
    'spike test response time < 1s': (r) => r.timings.duration < 1000,
  });
}

Step 4: Complex Real-World Scenario

tests/complex-scenario.js
import http from 'k6/http';
import { check, group, sleep } from 'k6';
import { Counter, Trend, Rate } from 'k6/metrics';
 
const checkoutSuccessRate = new Rate('checkout_success');
const inventoryErrors = new Counter('inventory_errors');
const paymentFailures = new Counter('payment_failures');
const checkoutDuration = new Trend('checkout_duration');
 
export const options = {
  scenarios: {
    browsers: {
      executor: 'ramping-vus',
      startVUs: 0,
      stages: [
        { duration: '2m', target: 100 },
        { duration: '5m', target: 100 },
        { duration: '2m', target: 0 },
      ],
      exec: 'browsing',
    },
    buyers: {
      executor: 'ramping-vus',
      startVUs: 0,
      stages: [
        { duration: '2m', target: 50 },
        { duration: '5m', target: 50 },
        { duration: '2m', target: 0 },
      ],
      exec: 'checkout',
    },
  },
  thresholds: {
    http_req_failed: ['rate<0.1'],
    checkout_success: ['rate>0.9'],
    checkout_duration: ['p(95)<2000'],
  },
};
 
export function browsing() {
  const baseUrl = 'http://localhost:3000';
  const sessionId = `browser-${__VU}-${Date.now()}`;
 
  group('Product Browsing', function () {
    // List products
    const listRes = http.get(`${baseUrl}/products?limit=20&offset=0`, {
      headers: { 'x-session-id': sessionId },
    });
 
    check(listRes, {
      'list status 200': (r) => r.status === 200,
    });
 
    sleep(2);
 
    // View random product
    const productId = Math.floor(Math.random() * 100) + 1;
    const detailRes = http.get(`${baseUrl}/products/${productId}`, {
      headers: { 'x-session-id': sessionId },
    });
 
    check(detailRes, {
      'detail status 200': (r) => r.status === 200,
    });
 
    sleep(3);
 
    // Check availability
    const availRes = http.get(
      `${baseUrl}/products/${productId}/availability?quantity=1`,
      {
        headers: { 'x-session-id': sessionId },
      }
    );
 
    check(availRes, {
      'availability status 200': (r) => r.status === 200,
    });
 
    sleep(1);
  });
}
 
export function checkout() {
  const baseUrl = 'http://localhost:3000';
  const sessionId = `buyer-${__VU}-${Date.now()}`;
  const startTime = new Date();
 
  group('Complete Checkout Flow', function () {
    // Browse products
    http.get(`${baseUrl}/products?limit=20`, {
      headers: { 'x-session-id': sessionId },
    });
 
    sleep(1);
 
    // Create order dengan multiple items
    const orderRes = http.post(
      `${baseUrl}/orders/create`,
      JSON.stringify({
        items: [
          { productId: 1, quantity: 2, price: 100 },
          { productId: 2, quantity: 1, price: 50 },
          { productId: 3, quantity: 1, price: 75 },
        ],
      }),
      {
        headers: {
          'x-session-id': sessionId,
          'Content-Type': 'application/json',
        },
      }
    );
 
    if (orderRes.status !== 200) {
      inventoryErrors.add(1);
      checkoutSuccessRate.add(0);
      return;
    }
 
    const orderId = orderRes.json('id');
    sleep(1);
 
    // Process payment
    const paymentRes = http.post(
      `${baseUrl}/orders/${orderId}/payment`,
      JSON.stringify({
        method: 'credit_card',
        amount: orderRes.json('totalAmount'),
      }),
      {
        headers: { 'Content-Type': 'application/json' },
      }
    );
 
    if (paymentRes.status !== 200 || paymentRes.json('paymentStatus') === 'failed') {
      paymentFailures.add(1);
      checkoutSuccessRate.add(0);
      return;
    }
 
    sleep(1);
 
    // Complete order
    const completeRes = http.post(`${baseUrl}/orders/${orderId}/complete`, null);
 
    const duration = new Date() - startTime;
    checkoutDuration.add(duration);
 
    checkoutSuccessRate.add(completeRes.status === 200 ? 1 : 0);
 
    check(completeRes, {
      'order completed': (r) => r.status === 200,
      'order status is completed': (r) => r.json('status') === 'completed',
    });
  });
}

Step 5: Running K6 Tests

Run K6 tests
# Install K6
# macOS
brew install k6
 
# Linux
sudo apt-get install k6
 
# Windows
choco install k6
 
# Run user journey test
k6 run tests/user-journey-test.js
 
# Run stress test
k6 run tests/stress-test.js
 
# Run spike test
k6 run tests/spike-test.js
 
# Run complex scenario
k6 run tests/complex-scenario.js
 
# Run dengan custom options
k6 run tests/user-journey-test.js --vus 50 --duration 5m
 
# Run dengan output
k6 run tests/user-journey-test.js --out json=results.json

Common Mistakes & Pitfalls

1. Not Ramping Up Load Gradually

js
// ❌ Wrong - sudden spike
export const options = {
  vus: 1000,
  duration: '1m',
};
 
// ✅ Correct - gradual ramp-up
export const options = {
  stages: [
    { duration: '2m', target: 100 },
    { duration: '2m', target: 500 },
    { duration: '2m', target: 1000 },
    { duration: '2m', target: 0 },
  ],
};

2. Ignoring Think Time

js
// ❌ Wrong - no delays between requests
export default function () {
  http.get('https://api.example.com/products');
  http.get('https://api.example.com/products/1');
  http.get('https://api.example.com/cart/add');
}
 
// ✅ Correct - realistic think time
export default function () {
  http.get('https://api.example.com/products');
  sleep(2);
  http.get('https://api.example.com/products/1');
  sleep(1);
  http.get('https://api.example.com/cart/add');
}

3. Not Using Realistic Data

js
// ❌ Wrong - same data every time
export default function () {
  http.post('https://api.example.com/orders', {
    productId: 1,
    quantity: 1,
  });
}
 
// ✅ Correct - varied data
export default function () {
  http.post('https://api.example.com/orders', {
    productId: Math.floor(Math.random() * 1000) + 1,
    quantity: Math.floor(Math.random() * 10) + 1,
  });
}

4. Not Checking Responses

js
// ❌ Wrong - no validation
export default function () {
  http.get('https://api.example.com/users');
}
 
// ✅ Correct - validate responses
export default function () {
  const res = http.get('https://api.example.com/users');
  check(res, {
    'status is 200': (r) => r.status === 200,
    'response time < 500ms': (r) => r.timings.duration < 500,
  });
}

Best Practices

1. Start Small, Scale Up

Mulai dengan small loads dan gradually increase untuk find breaking points.

js
export const options = {
  stages: [
    { duration: '1m', target: 10 },
    { duration: '1m', target: 50 },
    { duration: '1m', target: 100 },
    { duration: '1m', target: 0 },
  ],
};

2. Use Realistic Scenarios

Simulate actual user behavior dengan think time dan realistic workflows.

js
export default function () {
  group('Browse', () => {
    http.get('https://api.example.com/products');
    sleep(2);
  });
 
  group('View Details', () => {
    http.get('https://api.example.com/products/1');
    sleep(1);
  });
 
  group('Checkout', () => {
    http.post('https://api.example.com/checkout', {});
    sleep(1);
  });
}

3. Monitor Key Metrics

Track business-relevant metrics alongside technical metrics.

js
const checkoutSuccess = new Rate('checkout_success');
const paymentFailures = new Counter('payment_failures');
 
export default function () {
  // Track metrics
  checkoutSuccess.add(success ? 1 : 0);
  paymentFailures.add(failed ? 1 : 0);
}

4. Use Thresholds untuk CI/CD

Define thresholds untuk fail tests automatically.

js
export const options = {
  thresholds: {
    http_req_failed: ['rate<0.05'],
    http_req_duration: ['p(95)<500'],
    checkout_success: ['rate>0.95'],
  },
};

5. Test Different Scenarios

Run multiple test types untuk understand system behavior.

bash
# Load test
k6 run tests/load-test.js
 
# Stress test
k6 run tests/stress-test.js
 
# Spike test
k6 run tests/spike-test.js

Conclusion

K6 transforms performance testing dari specialized skill menjadi developer practice. Memahami load testing fundamentals—VUs, stages, checks, dan thresholds—enable Anda untuk build reliable systems.

E-commerce platform example demonstrate production patterns:

  • Realistic user journeys
  • Complex checkout flows
  • Inventory management under load
  • Payment processing simulation
  • Multiple concurrent scenarios
  • Business metric tracking

Key takeaways:

  1. Start dengan small loads dan scale gradually
  2. Simulate realistic user behavior
  3. Use checks dan thresholds untuk validation
  4. Monitor business metrics alongside technical metrics
  5. Run multiple test types (load, stress, spike, soak)
  6. Integrate tests ke CI/CD pipelines
  7. Analyze results thoroughly

Next steps:

  1. Install K6 locally
  2. Write basic load test untuk API Anda
  3. Add realistic think time dan workflows
  4. Define meaningful thresholds
  5. Integrate ke CI/CD pipeline Anda
  6. Run tests regularly untuk catch regressions

K6 makes performance testing accessible dan practical. Master it, dan Anda akan build systems yang scale reliably under real-world traffic patterns.


Related Posts