Generating Unit Tests with Kiro
I gave Kiro a real untested TypeScript module and asked it to generate tests. What happened next surprised me — it found a bug in the code, fixed it, and delivered 98% coverage.
The Problem We All Know
You ship a utility module. It works. It’s clean. But it has zero tests.
You tell yourself you’ll add them later. You won’t. Six months pass, someone changes a line, and something breaks silently in production. We’ve all been there.
I decided to run an experiment: take a real TypeScript module with no tests, hand it to Kiro (an AI-powered IDE), and see how far it could take me, from zero tests to comprehensive coverage. No hand-holding, no detailed prompting.
The goal: see if Kiro can meaningfully accelerate the testing workflow, not replace human judgment.
The Setup
I started by asking Kiro to scaffold a utility module for a URL shortener service. Something practical that any developer would relate to. I explicitly told it: do not write tests.
Create a TypeScript utility module for a URL shortener service.
Implement: generateShortCode, isValidUrl, normalizeUrl, extractDomain.
Do NOT write tests — I will add those separately.Kiro generated a clean project with TypeScript configured, compiled it, and even self-corrected along the way, when the first version had a URLSearchParams.keys() compatibility issue with the TypeScript lib config, it switched to a different iteration approach that worked.
The Code Under Test
Here’s what Kiro produced — four pure, well-typed, exported functions:
// src/urlShortener.tsconst ALPHANUMERIC = 'abcdefghijklmnopqrstuvwxyz0123456789';
const LETTERS = 'abcdefghijklmnopqrstuvwxyz';const TRACKING_PARAMS = new Set([
'utm_source', 'utm_medium', 'utm_campaign',
'utm_content', 'utm_term', 'fbclid', 'gclid',
]);export function generateShortCode(length: number = 6): string {
let code = LETTERS[Math.floor(Math.random() * LETTERS.length)];
for (let i = 1; i < length; i++) {
code += ALPHANUMERIC[Math.floor(Math.random() * ALPHANUMERIC.length)];
}
return code;
}export function isValidUrl(url: string): boolean {
if (!url || url.trim() === '') return false;
let parsed: URL;
try { parsed = new URL(url); } catch { return false; }
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return false;
if (!parsed.hostname || !parsed.hostname.includes('.')) return false;
return true;
}export function normalizeUrl(url: string): string {
const parsed = new URL(url);
parsed.protocol = parsed.protocol.toLowerCase();
parsed.hostname = parsed.hostname.toLowerCase(); if (
(parsed.protocol === 'http:' && parsed.port === '80') ||
(parsed.protocol === 'https:' && parsed.port === '443')
) {
parsed.port = '';
} const params = new URLSearchParams(parsed.search);
const keysToDelete: string[] = [];
params.forEach((_value, key) => {
if (TRACKING_PARAMS.has(key.toLowerCase())) keysToDelete.push(key);
});
keysToDelete.forEach((key) => params.delete(key));
parsed.search = params.toString(); let result = parsed.toString();
if (result.endsWith('/') && parsed.pathname !== '/') {
result = result.slice(0, -1);
}
if (parsed.pathname === '/' && !parsed.search && !parsed.hash) {
result = result.replace(/\/$/, '');
}
return result;
}export function extractDomain(url: string): string {
const parsed = new URL(url);
return parsed.hostname.toLowerCase();
}
Four functions. Zero tests. Let’s fix that.
Step 1: Asking Kiro to Generate Tests
With the module ready, I gave Kiro a straightforward prompt:
Generate comprehensive unit tests for all functions in src/urlShortener.ts
using Jest with TypeScript.
- Create the test file at src/urlShortener.test.ts
- Configure Jest (install jest, ts-jest, @types/jest)
- Group tests by function using describe blocks
- Include: happy path, edge cases, boundary conditions, error scenarios
- Make sure all tests pass when running `npm test`Kiro didn’t just write tests — it set up the entire infrastructure: installed dependencies, created jest.config.ts with ts-jest preset, and added the test script to package.json.
Then it generated 64 tests organized in clean describe blocks.
What the first pass covered:
Here are some highlights:
it('should never start with a digit', () => {
for (let i = 0; i < 200; i++) {
const code = generateShortCode();
expect(code[0]).toMatch(/[a-z]/);
}
});Kiro used statistical testing — running 200 iterations to verify the randomness constraint. Not something you’d see from a naive test generator.
it('should return false for javascript protocol', () => {
expect(isValidUrl('javascript:alert(1)')).toBe(false);
});it('should return false for data URI', () => {
expect(isValidUrl('data:text/html,<h1>hi</h1>')).toBe(false);
});Security-relevant edge cases I might not have thought of immediately.
The Plot Twist: Tests Found a Bug 🐛
Here’s where it gets interesting. When Kiro ran the tests, one failed:
FAIL src/urlShortener.test.ts
● normalizeUrl › trailing slashes › should remove trailing slash when query params follow Expected: "https://example.com/page?ref=home"
Received: "https://example.com/page/?ref=home"The test expected trailing slashes to be removed from paths even when query params are present. But the original normalizeUrl only checked result.endsWith('/') — which is never true when there are query params after the path.
Kiro identified this as a bug in the source code, not a bad test. It then fixed the implementation by moving the trailing slash logic to operate on parsed.pathname directly instead of the final string:
// Before (buggy):
let result = parsed.toString();
if (result.endsWith('/') && parsed.pathname !== '/') {
result = result.slice(0, -1);
}// After (fixed):
if (parsed.pathname.length > 1 && parsed.pathname.endsWith('/')) {
parsed.pathname = parsed.pathname.slice(0, -1);
}
let result = parsed.toString();After the fix: 64 tests, all passing. ✅
Get Vitor Hansen’s stories in your inbox
Join Medium for free to get updates from this writer.
This was the most impressive moment of the whole exercise. The generated tests didn’t just validate — they caught a real bug in the implementation and prompted Kiro to fix it. That’s exactly the value of testing.
Step 1: Prints
Step 2: Asking Kiro to Improve Coverage
After the initial success, I followed up:
Review the tests and improve coverage. Add tests for:
- Unicode/internationalized URLs
- Very long URLs (2000+ characters)
- URLs with authentication (user:password@host)
- Multiple tracking params mixed with valid params
- generateShortCode with edge case lengths (0, 1, 100)
- Concurrent calls producing unique codes
Add coverage reporting to jest.config.Kiro added 34 more tests and configured coverage with thresholds. Here are some of the new additions:
Uniqueness Under Pressure
it('should produce unique codes across 1000 concurrent-style calls', () => {
const codes = Array.from({ length: 1000 }, () => generateShortCode(8));
const unique = new Set(codes);
// With 26 × 36^7 ≈ 2 trillion possibilities, 1000 should never collide
expect(unique.size).toBe(1000);
});Unicode URLs
it('should return true for URL with unicode characters in path', () => {
expect(isValidUrl('https://example.com/café')).toBe(true);
});it('should return true for internationalized domain with dot', () => {
expect(isValidUrl('https://münchen.de/info')).toBe(true);
});Very Long URLs
it('should handle URL with very long query string and tracking params', () => {
const longValue = 'x'.repeat(2000);
const url = `https://example.com/page?data=${longValue}&utm_source=test`;
const result = normalizeUrl(url);
expect(result).toContain(`data=${longValue}`);
expect(result).not.toContain('utm_source');
});Mixed Tracking + Valid Params
it('should remove all tracking params while preserving valid ones', () => {
const url = 'https://example.com/page?utm_source=a&utm_medium=b&utm_campaign=c&utm_content=d&utm_term=e&fbclid=f&gclid=g&ref=home&page=2';
const result = normalizeUrl(url);
expect(result).not.toContain('utm_');
expect(result).not.toContain('fbclid');
expect(result).not.toContain('gclid');
expect(result).toContain('ref=home');
expect(result).toContain('page=2');
});Final Results
$ npm test -- --coveragePASS src/urlShortener.test.ts (98 tests)--------------------|---------|----------|---------|---------|
File | % Stmts | % Branch | % Funcs | % Lines |
--------------------|---------|----------|---------|---------|
All files | 97.72 | 96.66 | 100 | 97.61 |
urlShortener.ts | 97.72 | 96.66 | 100 | 97.61 |
--------------------|---------|----------|---------|---------|Tests: 98 passed, 98 total
Test Suites: 1 passed, 1 total
From zero to 98 tests and 97.72% statement coverage in two prompts.
Step 2: Prints
The Full Timeline
What Kiro Got Right
- ✅ Self-correcting — fixed TypeScript lib issues during setup, fixed the real bug it found
- ✅ Test organization — clean
describe/itblocks grouped by function and scenario type - ✅ Edge cases — empty strings, zero-length codes, 2000+ char URLs, Unicode, auth credentials
- ✅ Security-relevant scenarios —
javascript:protocol,data:URIs - ✅ Statistical testing — 200 iterations for randomness, 1000 calls for uniqueness
- ✅ Infrastructure — didn’t just write tests, configured the entire Jest pipeline
- ✅ Real bug detection — the trailing slash issue was a legitimate defect
What I Would Add Manually
Being honest — Kiro delivered a production-ready test suite. But if this were a critical library, I’d layer on:
- Property-based tests — using
fast-checkto verify invariants (e.g.,normalizeUrlshould be idempotent) - Snapshot tests — for a fixed set of URLs to catch regressions
- Performance benchmarks — ensuring 10,000 normalizations run in under X ms
- Integration tests — testing these utilities within a real Express handler
These are system-level decisions that require knowing the broader context. Kiro handles functional correctness; you bring the architecture judgment.
Tips for Getting Better Tests from Kiro
- Show the full implementation — Kiro needs the logic to find boundary conditions
- Ask for “comprehensive” tests — triggers edge case thinking
- Iterate — “What edge cases are missing?” adds 15% more coverage in seconds
- Let it self-correct — when a test fails, let Kiro figure out if it’s a bad test or a bad implementation
- Specify your framework — “Jest with TypeScript” avoids ambiguity
- Ask for coverage reporting — gives you a quantitative view of what’s missing
Try It Yourself
The full project is available on GitHub: kiro_unit_test_example
git clone https://github.com/YOUR_USERNAME/kiro_unit_test_example
cd kiro_unit_test_example
npm install
npm test -- --coverageNext time you have untested code gathering dust, give Kiro 5 minutes. You might end up with 98 tests, near-complete coverage, and — like me — a bug fix you didn’t expect.
Kiro is available now — check it out at kiro.dev.










