[AI] API Testing Với Kiro

Posted by : on

Category : ai   kiro


Chương 17: API Testing Với Kiro

Tóm Tắt

Hướng dẫn chi tiết cách sử dụng Kiro để tạo và quản lý API test suite, bao gồm REST API testing, GraphQL testing, và API contract testing.

Tạo API Test Suite

Spec Cho API Testing

requirements.md:

# Requirements: API Test Suite

## Test Coverage
- All REST endpoints
- Request/Response validation
- Authentication/Authorization
- Error handling
- Rate limiting
- Performance testing

## Test Scenarios

### TS-1: GET /api/users
**Given** valid authentication token
**When** request GET /api/users
**Then** should return 200 status
**And** response should match schema
**And** response time < 500ms

### TS-2: POST /api/users - Success
**Given** valid user data
**When** request POST /api/users
**Then** should return 201 status
**And** should create user in database
**And** response should include user id

### TS-3: POST /api/users - Validation Error
**Given** invalid email format
**When** request POST /api/users
**Then** should return 400 status
**And** error message should indicate invalid email

### TS-4: Authentication Required
**Given** no auth token
**When** request any protected endpoint
**Then** should return 401 status
**And** error message should indicate unauthorized

Test Framework Setup

Prompt Cho Kiro

Tạo API test framework với:

1. Framework: Jest + Supertest
2. TypeScript support
3. Test structure:
   - Request builders
   - Response validators
   - Schema validation
   - Test data factories
4. Features:
   - Authentication helpers
   - Database seeding
   - API mocking
   - Test reporting

Generated Structure

tests/
├── api/
│   ├── auth.spec.ts
│   ├── users.spec.ts
│   └── posts.spec.ts
├── helpers/
│   ├── ApiClient.ts
│   ├── AuthHelper.ts
│   └── DatabaseHelper.ts
├── schemas/
│   ├── UserSchema.ts
│   └── PostSchema.ts
├── fixtures/
│   ├── users.json
│   └── posts.json
└── setup.ts

API Test Examples

1. Basic GET Request

// tests/api/users.spec.ts
import request from 'supertest';
import { app } from '../../src/app';
import { AuthHelper } from '../helpers/AuthHelper';

describe('GET /api/users', () => {
  let authToken: string;

  beforeAll(async () => {
    authToken = await AuthHelper.getValidToken();
  });

  it('should return list of users', async () => {
    const response = await request(app)
      .get('/api/users')
      .set('Authorization', `Bearer ${authToken}`)
      .expect(200);

    expect(response.body).toHaveProperty('users');
    expect(Array.isArray(response.body.users)).toBe(true);
    expect(response.body.users.length).toBeGreaterThan(0);
  });

  it('should return 401 without auth token', async () => {
    const response = await request(app)
      .get('/api/users')
      .expect(401);

    expect(response.body.error).toBe('Unauthorized');
  });

  it('should respond within 500ms', async () => {
    const start = Date.now();
    
    await request(app)
      .get('/api/users')
      .set('Authorization', `Bearer ${authToken}`)
      .expect(200);
    
    const duration = Date.now() - start;
    expect(duration).toBeLessThan(500);
  });
});

2. POST Request With Validation

describe('POST /api/users', () => {
  it('should create user with valid data', async () => {
    const newUser = {
      email: 'newuser@example.com',
      password: 'SecurePass123',
      name: 'New User'
    };

    const response = await request(app)
      .post('/api/users')
      .set('Authorization', `Bearer ${adminToken}`)
      .send(newUser)
      .expect(201);

    expect(response.body).toMatchObject({
      email: newUser.email,
      name: newUser.name
    });
    expect(response.body).toHaveProperty('id');
    expect(response.body).not.toHaveProperty('password');
  });

  it('should return 400 with invalid email', async () => {
    const invalidUser = {
      email: 'invalid-email',
      password: 'SecurePass123'
    };

    const response = await request(app)
      .post('/api/users')
      .set('Authorization', `Bearer ${adminToken}`)
      .send(invalidUser)
      .expect(400);

    expect(response.body.errors).toContainEqual(
      expect.objectContaining({
        field: 'email',
        message: expect.stringContaining('Invalid email')
      })
    );
  });

  it('should return 400 with weak password', async () => {
    const weakPasswordUser = {
      email: 'user@example.com',
      password: '123'
    };

    const response = await request(app)
      .post('/api/users')
      .set('Authorization', `Bearer ${adminToken}`)
      .send(weakPasswordUser)
      .expect(400);

    expect(response.body.errors).toContainEqual(
      expect.objectContaining({
        field: 'password',
        message: expect.stringContaining('at least 8 characters')
      })
    );
  });
});

3. PUT/PATCH Request

describe('PUT /api/users/:id', () => {
  let userId: string;

  beforeEach(async () => {
    const user = await UserFactory.create();
    userId = user.id;
  });

  it('should update user', async () => {
    const updates = {
      name: 'Updated Name',
      bio: 'New bio'
    };

    const response = await request(app)
      .put(`/api/users/${userId}`)
      .set('Authorization', `Bearer ${authToken}`)
      .send(updates)
      .expect(200);

    expect(response.body.name).toBe(updates.name);
    expect(response.body.bio).toBe(updates.bio);
  });

  it('should return 404 for non-existent user', async () => {
    const fakeId = 'non-existent-id';

    await request(app)
      .put(`/api/users/${fakeId}`)
      .set('Authorization', `Bearer ${authToken}`)
      .send({ name: 'Test' })
      .expect(404);
  });

  it('should return 403 when updating other user', async () => {
    const otherUserToken = await AuthHelper.getTokenForUser('other@example.com');

    await request(app)
      .put(`/api/users/${userId}`)
      .set('Authorization', `Bearer ${otherUserToken}`)
      .send({ name: 'Hacked' })
      .expect(403);
  });
});

4. DELETE Request

describe('DELETE /api/users/:id', () => {
  it('should delete user', async () => {
    const user = await UserFactory.create();

    await request(app)
      .delete(`/api/users/${user.id}`)
      .set('Authorization', `Bearer ${adminToken}`)
      .expect(204);

    // Verify user is deleted
    const checkResponse = await request(app)
      .get(`/api/users/${user.id}`)
      .set('Authorization', `Bearer ${adminToken}`)
      .expect(404);
  });

  it('should require admin role', async () => {
    const user = await UserFactory.create();

    await request(app)
      .delete(`/api/users/${user.id}`)
      .set('Authorization', `Bearer ${regularUserToken}`)
      .expect(403);
  });
});

Schema Validation

Define Schemas

// tests/schemas/UserSchema.ts
import Ajv from 'ajv';

const ajv = new Ajv();

export const UserSchema = {
  type: 'object',
  required: ['id', 'email', 'name', 'createdAt'],
  properties: {
    id: { type: 'string', format: 'uuid' },
    email: { type: 'string', format: 'email' },
    name: { type: 'string', minLength: 1 },
    bio: { type: 'string' },
    role: { type: 'string', enum: ['user', 'admin'] },
    createdAt: { type: 'string', format: 'date-time' },
    updatedAt: { type: 'string', format: 'date-time' }
  },
  additionalProperties: false
};

export const validateUser = ajv.compile(UserSchema);

// Helper
export function expectValidUser(data: any) {
  const valid = validateUser(data);
  if (!valid) {
    throw new Error(`Invalid user schema: ${JSON.stringify(validateUser.errors)}`);
  }
}

Use In Tests

import { expectValidUser } from '../schemas/UserSchema';

it('should return valid user schema', async () => {
  const response = await request(app)
    .get('/api/users/123')
    .set('Authorization', `Bearer ${authToken}`)
    .expect(200);

  expectValidUser(response.body);
});

Test Helpers

API Client Helper

// tests/helpers/ApiClient.ts
import request from 'supertest';
import { app } from '../../src/app';

export class ApiClient {
  private authToken?: string;

  setAuthToken(token: string) {
    this.authToken = token;
  }

  async get(path: string, expectedStatus = 200) {
    const req = request(app).get(path);
    
    if (this.authToken) {
      req.set('Authorization', `Bearer ${this.authToken}`);
    }
    
    return req.expect(expectedStatus);
  }

  async post(path: string, data: any, expectedStatus = 201) {
    const req = request(app).post(path).send(data);
    
    if (this.authToken) {
      req.set('Authorization', `Bearer ${this.authToken}`);
    }
    
    return req.expect(expectedStatus);
  }

  async put(path: string, data: any, expectedStatus = 200) {
    const req = request(app).put(path).send(data);
    
    if (this.authToken) {
      req.set('Authorization', `Bearer ${this.authToken}`);
    }
    
    return req.expect(expectedStatus);
  }

  async delete(path: string, expectedStatus = 204) {
    const req = request(app).delete(path);
    
    if (this.authToken) {
      req.set('Authorization', `Bearer ${this.authToken}`);
    }
    
    return req.expect(expectedStatus);
  }
}

Auth Helper

// tests/helpers/AuthHelper.ts
import jwt from 'jsonwebtoken';
import { UserFactory } from '../factories/UserFactory';

export class AuthHelper {
  static async getValidToken(role: 'user' | 'admin' = 'user'): Promise<string> {
    const user = await UserFactory.create({ role });
    return this.generateToken(user.id, role);
  }

  static generateToken(userId: string, role: string): string {
    return jwt.sign(
      { userId, role },
      process.env.JWT_SECRET!,
      { expiresIn: '1h' }
    );
  }

  static async getTokenForUser(email: string): Promise<string> {
    const user = await UserFactory.findByEmail(email);
    return this.generateToken(user.id, user.role);
  }
}

Database Helper

// tests/helpers/DatabaseHelper.ts
import { prisma } from '../../src/lib/prisma';

export class DatabaseHelper {
  static async clearDatabase() {
    await prisma.post.deleteMany();
    await prisma.user.deleteMany();
  }

  static async seedTestData() {
    await UserFactory.createBatch(10);
    await PostFactory.createBatch(20);
  }

  static async resetDatabase() {
    await this.clearDatabase();
    await this.seedTestData();
  }
}

Advanced Testing Patterns

1. Pagination Testing

describe('GET /api/users - Pagination', () => {
  beforeAll(async () => {
    await UserFactory.createBatch(50);
  });

  it('should return first page', async () => {
    const response = await request(app)
      .get('/api/users?page=1&limit=10')
      .set('Authorization', `Bearer ${authToken}`)
      .expect(200);

    expect(response.body.users).toHaveLength(10);
    expect(response.body.pagination).toMatchObject({
      page: 1,
      limit: 10,
      total: 50,
      totalPages: 5
    });
  });

  it('should return last page', async () => {
    const response = await request(app)
      .get('/api/users?page=5&limit=10')
      .set('Authorization', `Bearer ${authToken}`)
      .expect(200);

    expect(response.body.users).toHaveLength(10);
    expect(response.body.pagination.page).toBe(5);
  });
});

2. Filtering Testing

describe('GET /api/users - Filtering', () => {
  it('should filter by role', async () => {
    await UserFactory.create({ role: 'admin' });
    await UserFactory.create({ role: 'user' });

    const response = await request(app)
      .get('/api/users?role=admin')
      .set('Authorization', `Bearer ${authToken}`)
      .expect(200);

    expect(response.body.users.every(u => u.role === 'admin')).toBe(true);
  });

  it('should filter by multiple criteria', async () => {
    const response = await request(app)
      .get('/api/users?role=admin&status=active')
      .set('Authorization', `Bearer ${authToken}`)
      .expect(200);

    expect(response.body.users.every(u => 
      u.role === 'admin' && u.status === 'active'
    )).toBe(true);
  });
});

3. Sorting Testing

describe('GET /api/users - Sorting', () => {
  it('should sort by createdAt descending', async () => {
    const response = await request(app)
      .get('/api/users?sort=-createdAt')
      .set('Authorization', `Bearer ${authToken}`)
      .expect(200);

    const dates = response.body.users.map(u => new Date(u.createdAt));
    const sorted = [...dates].sort((a, b) => b.getTime() - a.getTime());
    
    expect(dates).toEqual(sorted);
  });
});

4. Rate Limiting Testing

describe('Rate Limiting', () => {
  it('should rate limit after 100 requests', async () => {
    const requests = Array(101).fill(null).map(() =>
      request(app)
        .get('/api/users')
        .set('Authorization', `Bearer ${authToken}`)
    );

    const responses = await Promise.all(requests);
    const lastResponse = responses[responses.length - 1];

    expect(lastResponse.status).toBe(429);
    expect(lastResponse.body.error).toContain('Too many requests');
  });
});

Contract Testing

OpenAPI Validation

import SwaggerParser from '@apidevtools/swagger-parser';
import { validateAgainstSchema } from 'openapi-validator';

describe('API Contract Tests', () => {
  let apiSpec: any;

  beforeAll(async () => {
    apiSpec = await SwaggerParser.validate('./docs/openapi.yaml');
  });

  it('GET /api/users should match OpenAPI spec', async () => {
    const response = await request(app)
      .get('/api/users')
      .set('Authorization', `Bearer ${authToken}`)
      .expect(200);

    const errors = validateAgainstSchema(
      response.body,
      apiSpec.paths['/api/users'].get.responses['200']
    );

    expect(errors).toHaveLength(0);
  });
});

Performance Testing

describe('Performance Tests', () => {
  it('should handle 100 concurrent requests', async () => {
    const requests = Array(100).fill(null).map(() =>
      request(app)
        .get('/api/users')
        .set('Authorization', `Bearer ${authToken}`)
    );

    const start = Date.now();
    const responses = await Promise.all(requests);
    const duration = Date.now() - start;

    expect(responses.every(r => r.status === 200)).toBe(true);
    expect(duration).toBeLessThan(5000); // 5 seconds for 100 requests
  });

  it('should respond quickly under load', async () => {
    const responseTimes: number[] = [];

    for (let i = 0; i < 50; i++) {
      const start = Date.now();
      await request(app)
        .get('/api/users')
        .set('Authorization', `Bearer ${authToken}`);
      responseTimes.push(Date.now() - start);
    }

    const avgResponseTime = responseTimes.reduce((a, b) => a + b) / responseTimes.length;
    const p95 = responseTimes.sort()[Math.floor(responseTimes.length * 0.95)];

    expect(avgResponseTime).toBeLessThan(200);
    expect(p95).toBeLessThan(500);
  });
});

Kết Luận

API testing với Kiro giúp:

  • Tự động generate test cases
  • Validate request/response schemas
  • Test authentication và authorization
  • Performance và load testing
  • Contract testing với OpenAPI

Chương tiếp theo: UI Testing và E2E Testing


Bài viết được viết bằng AI 🚀


About Nguyen Chung
Nguyen Chung

Hi I am Nguyen Chung, an Automation Tester.

Email : ndchungict@gmail.com

Website : https://ndchungict.github.io

About Nguyen Chung

Hi, my name is Nguyen Duc Chung. Nice to see you!