Claude Code Plugins

Community-maintained marketplace

Feedback

Write tests with TDD. Supports Jest, Cypress, Detox, PHPUnit, PyTest, Go testing.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name test-writer
description Write tests with TDD. Supports Jest, Cypress, Detox, PHPUnit, PyTest, Go testing.
autoInvoke true
priority medium
triggers add tests, test coverage, unit test, E2E test, write tests
allowed-tools Read, Grep, Glob, Edit, Write, Bash

Aura Frog Test Writer

Priority: MEDIUM - Use for test-related requests Version: 1.0.0


When to Use

USE for:

  • Adding tests to existing code
  • Improving test coverage
  • Creating test suites
  • TDD implementation (Phase 5a)
  • Writing specific test types (unit, integration, E2E)

DON'T use for:

  • Bug fixes without explicit test request → use bugfix-quick
  • Full feature implementation → use workflow-orchestrator

Test Writing Process

1. Analyze Target Code

1. Read file with Read tool
2. Identify testable units:
   - Functions/methods
   - Components
   - API endpoints
   - Data transformations
3. List dependencies to mock

2. Plan Strategy

Type Use For Scope
Unit Individual functions/components Single unit, mocked deps
Integration Module interactions, API calls Multiple units together
E2E Complete user flows Full system, real deps

3. Write Tests

For NEW code (TDD - Phase 5a):

1. Write failing tests (RED)
   → Tests MUST fail
   → If they pass, tests are wrong
2. Implement code (GREEN)
   → Minimal code to pass
3. Refactor (REFACTOR)
   → Tests must stay green

For EXISTING code:

1. Write tests that pass (validate current behavior)
2. Add edge case tests
3. Add negative tests (error handling)

4. Verify Coverage

# Check target: 80% or project-specific
npm test -- --coverage          # JavaScript/TypeScript
pytest --cov=. --cov-report=html # Python
./vendor/bin/phpunit --coverage-html coverage # PHP
go test -coverprofile=coverage.out ./... # Go

Framework-Specific Templates

JavaScript/TypeScript - Jest

Unit Test (Function):

describe('calculateDiscount', () => {
  it('should apply 10% discount for orders over $100', () => {
    expect(calculateDiscount(150)).toBe(135);
  });

  it('should return original price for orders under $100', () => {
    expect(calculateDiscount(50)).toBe(50);
  });

  it('should throw error for negative amounts', () => {
    expect(() => calculateDiscount(-10)).toThrow('Invalid amount');
  });
});

Unit Test (React Component):

import { render, fireEvent, screen } from '@testing-library/react';
import { LoginButton } from './LoginButton';

describe('LoginButton', () => {
  it('should call onLogin when clicked', () => {
    const onLogin = jest.fn();
    render(<LoginButton onLogin={onLogin} />);

    fireEvent.click(screen.getByRole('button', { name: /login/i }));

    expect(onLogin).toHaveBeenCalledTimes(1);
  });

  it('should show loading state', () => {
    render(<LoginButton isLoading={true} />);

    expect(screen.getByTestId('loading-spinner')).toBeVisible();
  });

  it('should be disabled when loading', () => {
    render(<LoginButton isLoading={true} />);

    expect(screen.getByRole('button')).toBeDisabled();
  });
});

React Native - Jest + React Native Testing Library

Component Test:

import { render, fireEvent } from '@testing-library/react-native';
import { PaymentCard } from './PaymentCard';

describe('PaymentCard', () => {
  it('should display amount in correct format', () => {
    const { getByTestId } = render(<PaymentCard amount={1500.50} />);

    expect(getByTestId('amount-display')).toHaveTextContent('$1,500.50');
  });

  it('should call onPay when pay button pressed', () => {
    const onPay = jest.fn();
    const { getByTestId } = render(<PaymentCard onPay={onPay} />);

    fireEvent.press(getByTestId('pay-button'));

    expect(onPay).toHaveBeenCalledTimes(1);
  });
});

React Native - Detox E2E

E2E Test:

describe('Login Flow', () => {
  beforeAll(async () => {
    await device.launchApp();
  });

  beforeEach(async () => {
    await device.reloadReactNative();
  });

  it('should login successfully with valid credentials', async () => {
    await element(by.id('email-input')).typeText('user@example.com');
    await element(by.id('password-input')).typeText('securePassword123');
    await element(by.id('login-button')).tap();

    await expect(element(by.id('home-screen'))).toBeVisible();
    await expect(element(by.text('Welcome'))).toBeVisible();
  });

  it('should show error for invalid credentials', async () => {
    await element(by.id('email-input')).typeText('wrong@email.com');
    await element(by.id('password-input')).typeText('wrongpass');
    await element(by.id('login-button')).tap();

    await expect(element(by.text('Invalid credentials'))).toBeVisible();
  });
});

PHP - PHPUnit

Unit Test:

<?php

namespace Tests\Unit;

use PHPUnit\Framework\TestCase;
use App\Services\OrderCalculator;

class OrderCalculatorTest extends TestCase
{
    private OrderCalculator $calculator;

    protected function setUp(): void
    {
        $this->calculator = new OrderCalculator();
    }

    public function test_calculates_subtotal_correctly(): void
    {
        $items = [
            ['price' => 100, 'quantity' => 2],
            ['price' => 50, 'quantity' => 1],
        ];

        $result = $this->calculator->calculateSubtotal($items);

        $this->assertEquals(250, $result);
    }

    public function test_applies_discount_percentage(): void
    {
        $result = $this->calculator->applyDiscount(100, 10);

        $this->assertEquals(90, $result);
    }

    public function test_throws_exception_for_negative_discount(): void
    {
        $this->expectException(\InvalidArgumentException::class);
        $this->expectExceptionMessage('Discount cannot be negative');

        $this->calculator->applyDiscount(100, -5);
    }
}

Laravel Feature Test:

<?php

namespace Tests\Feature;

use Tests\TestCase;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;

class AuthenticationTest extends TestCase
{
    use RefreshDatabase;

    public function test_user_can_login_with_correct_credentials(): void
    {
        $user = User::factory()->create([
            'email' => 'test@example.com',
            'password' => bcrypt('password123'),
        ]);

        $response = $this->postJson('/api/login', [
            'email' => 'test@example.com',
            'password' => 'password123',
        ]);

        $response->assertStatus(200)
                 ->assertJsonStructure(['token', 'user']);
    }

    public function test_user_cannot_login_with_wrong_password(): void
    {
        $user = User::factory()->create([
            'email' => 'test@example.com',
            'password' => bcrypt('password123'),
        ]);

        $response = $this->postJson('/api/login', [
            'email' => 'test@example.com',
            'password' => 'wrongpassword',
        ]);

        $response->assertStatus(401)
                 ->assertJson(['message' => 'Invalid credentials']);
    }
}

Python - PyTest

Unit Test:

import pytest
from app.services.calculator import OrderCalculator


class TestOrderCalculator:
    @pytest.fixture
    def calculator(self):
        return OrderCalculator()

    def test_calculate_subtotal(self, calculator):
        items = [
            {"price": 100, "quantity": 2},
            {"price": 50, "quantity": 1},
        ]

        result = calculator.calculate_subtotal(items)

        assert result == 250

    def test_apply_discount_percentage(self, calculator):
        result = calculator.apply_discount(100, 10)

        assert result == 90

    def test_raises_error_for_negative_discount(self, calculator):
        with pytest.raises(ValueError, match="Discount cannot be negative"):
            calculator.apply_discount(100, -5)

    @pytest.mark.parametrize("amount,discount,expected", [
        (100, 0, 100),
        (100, 10, 90),
        (100, 50, 50),
        (100, 100, 0),
    ])
    def test_discount_edge_cases(self, calculator, amount, discount, expected):
        assert calculator.apply_discount(amount, discount) == expected

FastAPI Integration Test:

import pytest
from fastapi.testclient import TestClient
from app.main import app
from app.models import User


@pytest.fixture
def client():
    return TestClient(app)


@pytest.fixture
def test_user(db_session):
    user = User(email="test@example.com", password="hashed_password")
    db_session.add(user)
    db_session.commit()
    return user


class TestAuthenticationAPI:
    def test_login_success(self, client, test_user):
        response = client.post("/api/login", json={
            "email": "test@example.com",
            "password": "password123"
        })

        assert response.status_code == 200
        assert "token" in response.json()
        assert "user" in response.json()

    def test_login_wrong_password(self, client, test_user):
        response = client.post("/api/login", json={
            "email": "test@example.com",
            "password": "wrongpassword"
        })

        assert response.status_code == 401
        assert response.json()["detail"] == "Invalid credentials"

    def test_login_missing_fields(self, client):
        response = client.post("/api/login", json={})

        assert response.status_code == 422

Go - Go Testing

Unit Test:

package calculator

import (
    "testing"
)

func TestCalculateSubtotal(t *testing.T) {
    calc := NewOrderCalculator()
    items := []Item{
        {Price: 100, Quantity: 2},
        {Price: 50, Quantity: 1},
    }

    result := calc.CalculateSubtotal(items)

    if result != 250 {
        t.Errorf("Expected 250, got %d", result)
    }
}

func TestApplyDiscount(t *testing.T) {
    calc := NewOrderCalculator()

    result := calc.ApplyDiscount(100, 10)

    if result != 90 {
        t.Errorf("Expected 90, got %d", result)
    }
}

func TestApplyDiscountNegative(t *testing.T) {
    calc := NewOrderCalculator()

    defer func() {
        if r := recover(); r == nil {
            t.Errorf("Expected panic for negative discount")
        }
    }()

    calc.ApplyDiscount(100, -5)
}

// Table-driven test
func TestApplyDiscountEdgeCases(t *testing.T) {
    calc := NewOrderCalculator()

    tests := []struct {
        name     string
        amount   int
        discount int
        expected int
    }{
        {"no discount", 100, 0, 100},
        {"10% discount", 100, 10, 90},
        {"50% discount", 100, 50, 50},
        {"full discount", 100, 100, 0},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := calc.ApplyDiscount(tt.amount, tt.discount)
            if result != tt.expected {
                t.Errorf("Expected %d, got %d", tt.expected, result)
            }
        })
    }
}

Coverage Targets

Type Target Rationale
Critical paths 100% Auth, payment, security
Business logic 90% Core domain logic
UI/Utilities 80% User-facing components
Overall 80% Project minimum (or custom)

Test File Naming Conventions

Framework Test File Location
Jest *.test.ts, *.spec.ts __tests__/ or alongside
PHPUnit *Test.php tests/Unit/, tests/Feature/
PyTest test_*.py, *_test.py tests/
Go *_test.go Same package
Detox *.e2e.ts e2e/
Cypress *.cy.ts cypress/e2e/

Running Tests by Framework

# JavaScript/TypeScript (Jest)
npm test
npm test -- --coverage
npm test -- --watch

# PHP (PHPUnit)
./vendor/bin/phpunit
./vendor/bin/phpunit --coverage-html coverage
./vendor/bin/phpunit --filter TestClassName

# Python (PyTest)
pytest
pytest --cov=. --cov-report=html
pytest -k "test_function_name"

# Go
go test ./...
go test -v ./...
go test -coverprofile=coverage.out ./...

# React Native (Detox)
detox test --configuration ios.sim.debug
detox test --configuration android.emu.debug

Remember:

  • Tests are documentation - write clear, maintainable tests
  • Follow AAA pattern: Arrange, Act, Assert
  • One assertion concept per test (can have multiple expects)
  • Test behavior, not implementation
  • Mock external dependencies