Back to list
mesbahtanvir

test-generator

by mesbahtanvir

ishkul.org

1🍴 0📅 Jan 18, 2026

SKILL.md


name: test-generator description: Generates comprehensive tests for Ishkul code including state transition tests that catch React Rules of Hooks violations. Creates tests for frontend screens/components (Jest + RTL) and backend handlers (Go testing + testify). Use when creating new code or adding test coverage.

Test Generator

Generates comprehensive tests for Ishkul's React Native frontend and Go backend.

Tech Stack

  • Frontend: Jest, React Testing Library, React Native Testing Library
  • Backend: Go testing, testify (assert/require)
  • E2E: Playwright (web), Maestro (mobile), Newman (API)

Frontend Test Generation

Test File Locations

frontend/src/
├── screens/__tests__/ScreenName.test.tsx
├── components/__tests__/ComponentName.test.tsx
├── hooks/__tests__/useHookName.test.ts
└── utils/__tests__/utilName.test.ts

Screen Test Template

Generate tests for screens with all states and transitions:

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

// Mock dependencies
jest.mock('../../hooks/useTheme', () => ({
  useTheme: () => ({
    colors: {
      primary: '#0066FF',
      background: '#FFFFFF',
      text: '#1A1A1A',
      error: '#DC2626',
    },
  }),
}));

jest.mock('../../state/lessonStore', () => ({
  useLessonStore: jest.fn(),
}));

jest.mock('@react-navigation/native', () => ({
  useNavigation: () => ({
    navigate: jest.fn(),
    goBack: jest.fn(),
  }),
  useRoute: () => ({
    params: { id: 'test-id' },
  }),
}));

import { useLessonStore } from '../../state/lessonStore';

const mockUseLessonStore = useLessonStore as jest.MockedFunction<typeof useLessonStore>;

describe('ScreenName', () => {
  const defaultProps = {
    navigation: { navigate: jest.fn(), goBack: jest.fn() },
    route: { params: { id: 'test-id' } },
  };

  beforeEach(() => {
    jest.clearAllMocks();
  });

  describe('Loading State', () => {
    it('should display loading indicator when loading', () => {
      mockUseLessonStore.mockReturnValue({
        currentLesson: null,
        lessonLoading: true,
        error: null,
      });

      const { getByTestId, queryByText } = render(<ScreenName {...defaultProps} />);

      expect(getByTestId('loading-indicator')).toBeTruthy();
      expect(queryByText(/error/i)).toBeNull();
    });

    it('should not display content while loading', () => {
      mockUseLessonStore.mockReturnValue({
        currentLesson: null,
        lessonLoading: true,
        error: null,
      });

      const { queryByTestId } = render(<ScreenName {...defaultProps} />);

      expect(queryByTestId('content-container')).toBeNull();
    });
  });

  describe('Error State', () => {
    it('should display error message when error occurs', () => {
      mockUseLessonStore.mockReturnValue({
        currentLesson: null,
        lessonLoading: false,
        error: 'Failed to load lesson',
      });

      const { getByText } = render(<ScreenName {...defaultProps} />);

      expect(getByText(/failed to load/i)).toBeTruthy();
    });

    it('should display retry button on error', () => {
      mockUseLessonStore.mockReturnValue({
        currentLesson: null,
        lessonLoading: false,
        error: 'Network error',
      });

      const { getByText } = render(<ScreenName {...defaultProps} />);

      expect(getByText(/retry/i)).toBeTruthy();
    });

    it('should call refetch when retry pressed', () => {
      const mockFetch = jest.fn();
      mockUseLessonStore.mockReturnValue({
        currentLesson: null,
        lessonLoading: false,
        error: 'Error',
        fetchLesson: mockFetch,
      });

      const { getByText } = render(<ScreenName {...defaultProps} />);
      fireEvent.press(getByText(/retry/i));

      expect(mockFetch).toHaveBeenCalled();
    });
  });

  describe('Empty State', () => {
    it('should display empty message when no data', () => {
      mockUseLessonStore.mockReturnValue({
        currentLesson: null,
        lessonLoading: false,
        error: null,
      });

      const { getByText } = render(<ScreenName {...defaultProps} />);

      expect(getByText(/no lesson found/i)).toBeTruthy();
    });
  });

  describe('Success State', () => {
    const mockLesson = {
      id: 'lesson-1',
      title: 'Test Lesson',
      blocks: [{ id: 'block-1', type: 'text', title: 'Block 1' }],
    };

    it('should display lesson content when loaded', () => {
      mockUseLessonStore.mockReturnValue({
        currentLesson: mockLesson,
        lessonLoading: false,
        error: null,
      });

      const { getByText } = render(<ScreenName {...defaultProps} />);

      expect(getByText('Test Lesson')).toBeTruthy();
    });

    it('should display all blocks', () => {
      mockUseLessonStore.mockReturnValue({
        currentLesson: mockLesson,
        lessonLoading: false,
        error: null,
      });

      const { getByText } = render(<ScreenName {...defaultProps} />);

      expect(getByText('Block 1')).toBeTruthy();
    });
  });

  // CRITICAL: State Transition Tests (Prevents React Hooks Violations)
  describe('State Transitions (Rules of Hooks)', () => {
    it('should handle transition from loading to success without crashing', () => {
      // Start with loading state
      mockUseLessonStore.mockReturnValue({
        currentLesson: null,
        lessonLoading: true,
        error: null,
      });

      const { rerender, getByTestId, getByText } = render(<ScreenName {...defaultProps} />);

      // Verify loading state
      expect(getByTestId('loading-indicator')).toBeTruthy();

      // Transition to success state
      mockUseLessonStore.mockReturnValue({
        currentLesson: { id: '1', title: 'Loaded Lesson', blocks: [] },
        lessonLoading: false,
        error: null,
      });

      // Should NOT crash on rerender
      expect(() => rerender(<ScreenName {...defaultProps} />)).not.toThrow();

      // Verify success state
      expect(getByText('Loaded Lesson')).toBeTruthy();
    });

    it('should handle transition from loading to error without crashing', () => {
      mockUseLessonStore.mockReturnValue({
        currentLesson: null,
        lessonLoading: true,
        error: null,
      });

      const { rerender, getByTestId, getByText } = render(<ScreenName {...defaultProps} />);

      expect(getByTestId('loading-indicator')).toBeTruthy();

      // Transition to error state
      mockUseLessonStore.mockReturnValue({
        currentLesson: null,
        lessonLoading: false,
        error: 'Network error',
      });

      expect(() => rerender(<ScreenName {...defaultProps} />)).not.toThrow();
      expect(getByText(/network error/i)).toBeTruthy();
    });

    it('should handle transition from error to loading (retry)', () => {
      mockUseLessonStore.mockReturnValue({
        currentLesson: null,
        lessonLoading: false,
        error: 'Error',
      });

      const { rerender, getByText, getByTestId } = render(<ScreenName {...defaultProps} />);

      expect(getByText(/error/i)).toBeTruthy();

      // User triggers retry
      mockUseLessonStore.mockReturnValue({
        currentLesson: null,
        lessonLoading: true,
        error: null,
      });

      expect(() => rerender(<ScreenName {...defaultProps} />)).not.toThrow();
      expect(getByTestId('loading-indicator')).toBeTruthy();
    });

    it('should handle rapid state changes without crashing', async () => {
      const states = [
        { currentLesson: null, lessonLoading: true, error: null },
        { currentLesson: null, lessonLoading: false, error: 'Error' },
        { currentLesson: null, lessonLoading: true, error: null },
        { currentLesson: { id: '1', title: 'Lesson', blocks: [] }, lessonLoading: false, error: null },
      ];

      mockUseLessonStore.mockReturnValue(states[0]);
      const { rerender } = render(<ScreenName {...defaultProps} />);

      for (const state of states.slice(1)) {
        mockUseLessonStore.mockReturnValue(state);
        expect(() => rerender(<ScreenName {...defaultProps} />)).not.toThrow();
      }
    });
  });

  describe('User Interactions', () => {
    it('should navigate back when back button pressed', () => {
      const mockGoBack = jest.fn();
      const props = {
        ...defaultProps,
        navigation: { ...defaultProps.navigation, goBack: mockGoBack },
      };

      mockUseLessonStore.mockReturnValue({
        currentLesson: { id: '1', title: 'Lesson', blocks: [] },
        lessonLoading: false,
        error: null,
      });

      const { getByTestId } = render(<ScreenName {...props} />);
      fireEvent.press(getByTestId('back-button'));

      expect(mockGoBack).toHaveBeenCalled();
    });
  });
});

Component Test Template

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

jest.mock('../../hooks/useTheme', () => ({
  useTheme: () => ({
    colors: { primary: '#0066FF', text: '#1A1A1A' },
  }),
}));

describe('Button', () => {
  describe('Rendering', () => {
    it('should render with title', () => {
      const { getByText } = render(<Button title="Click me" onPress={() => {}} />);
      expect(getByText('Click me')).toBeTruthy();
    });

    it('should render loading indicator when loading', () => {
      const { queryByText, getByTestId } = render(
        <Button title="Submit" onPress={() => {}} loading />
      );
      expect(queryByText('Submit')).toBeNull();
      expect(getByTestId('loading-indicator')).toBeTruthy();
    });
  });

  describe('Variants', () => {
    const variants = ['primary', 'secondary', 'outline', 'ghost', 'danger'] as const;

    variants.forEach((variant) => {
      it(`should render ${variant} variant`, () => {
        const { getByText } = render(
          <Button title={variant} onPress={() => {}} variant={variant} />
        );
        expect(getByText(variant)).toBeTruthy();
      });
    });
  });

  describe('Sizes', () => {
    const sizes = ['small', 'medium', 'large'] as const;

    sizes.forEach((size) => {
      it(`should render ${size} size`, () => {
        const { getByText } = render(
          <Button title={size} onPress={() => {}} size={size} />
        );
        expect(getByText(size)).toBeTruthy();
      });
    });
  });

  describe('Interactions', () => {
    it('should call onPress when pressed', () => {
      const onPressMock = jest.fn();
      const { getByText } = render(<Button title="Press me" onPress={onPressMock} />);

      fireEvent.press(getByText('Press me'));

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

    it('should not call onPress when disabled', () => {
      const onPressMock = jest.fn();
      const { getByText } = render(
        <Button title="Disabled" onPress={onPressMock} disabled />
      );

      fireEvent.press(getByText('Disabled'));

      expect(onPressMock).not.toHaveBeenCalled();
    });

    it('should not call onPress when loading', () => {
      const onPressMock = jest.fn();
      const { getByTestId } = render(
        <Button title="Loading" onPress={onPressMock} loading />
      );

      fireEvent.press(getByTestId('loading-indicator'));

      expect(onPressMock).not.toHaveBeenCalled();
    });
  });

  describe('Edge Cases', () => {
    it('should handle empty title', () => {
      const { container } = render(<Button title="" onPress={() => {}} />);
      expect(container).toBeTruthy();
    });

    it('should handle very long title', () => {
      const longTitle = 'A'.repeat(100);
      const { getByText } = render(<Button title={longTitle} onPress={() => {}} />);
      expect(getByText(longTitle)).toBeTruthy();
    });
  });
});

Backend Test Generation

Test File Location

backend/internal/handlers/handler_name_test.go

Handler Test Template

package handlers

import (
    "bytes"
    "context"
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/mesbahtanvir/ishkul/backend/internal/middleware"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

// Helper to create authenticated request
func createRequest(method, path string, body interface{}, userID, email string) *http.Request {
    var req *http.Request

    if body != nil {
        bodyBytes, _ := json.Marshal(body)
        req = httptest.NewRequest(method, path, bytes.NewBuffer(bodyBytes))
        req.Header.Set("Content-Type", "application/json")
    } else {
        req = httptest.NewRequest(method, path, nil)
    }

    if userID != "" {
        ctx := context.WithValue(req.Context(), middleware.UserIDKey, userID)
        ctx = context.WithValue(ctx, middleware.UserEmailKey, email)
        req = req.WithContext(ctx)
    }

    return req
}

func TestHandlerName(t *testing.T) {
    t.Run("Method Validation", func(t *testing.T) {
        unsupportedMethods := []string{http.MethodGet, http.MethodPut, http.MethodDelete}

        for _, method := range unsupportedMethods {
            t.Run(method+" should be rejected", func(t *testing.T) {
                req := createRequest(method, "/api/resource", nil, "user123", "test@example.com")
                rr := httptest.NewRecorder()

                HandlerName(rr, req)

                assert.Equal(t, http.StatusMethodNotAllowed, rr.Code)
            })
        }
    })

    t.Run("Authentication", func(t *testing.T) {
        t.Run("should reject unauthenticated request", func(t *testing.T) {
            req := createRequest(http.MethodPost, "/api/resource", nil, "", "")
            rr := httptest.NewRecorder()

            HandlerName(rr, req)

            assert.Equal(t, http.StatusUnauthorized, rr.Code)
        })
    })

    t.Run("Input Validation", func(t *testing.T) {
        t.Run("should reject empty body", func(t *testing.T) {
            req := httptest.NewRequest(http.MethodPost, "/api/resource", nil)
            ctx := context.WithValue(req.Context(), middleware.UserIDKey, "user123")
            req = req.WithContext(ctx)
            rr := httptest.NewRecorder()

            HandlerName(rr, req)

            assert.Equal(t, http.StatusBadRequest, rr.Code)

            var response ErrorResponse
            json.Unmarshal(rr.Body.Bytes(), &response)
            assert.Equal(t, ErrCodeInvalidRequest, response.Code)
        })

        t.Run("should reject invalid JSON", func(t *testing.T) {
            req := createRequest(http.MethodPost, "/api/resource", nil, "user123", "test@example.com")
            req.Body = io.NopCloser(bytes.NewBufferString("{invalid json"))
            rr := httptest.NewRecorder()

            HandlerName(rr, req)

            assert.Equal(t, http.StatusBadRequest, rr.Code)
        })

        t.Run("should reject missing required field", func(t *testing.T) {
            body := map[string]interface{}{
                "optionalField": "value",
                // missing "requiredField"
            }
            req := createRequest(http.MethodPost, "/api/resource", body, "user123", "test@example.com")
            rr := httptest.NewRecorder()

            HandlerName(rr, req)

            assert.Equal(t, http.StatusBadRequest, rr.Code)
        })
    })

    t.Run("Success Cases", func(t *testing.T) {
        t.Run("should create resource successfully", func(t *testing.T) {
            body := map[string]interface{}{
                "requiredField": "value",
            }
            req := createRequest(http.MethodPost, "/api/resource", body, "user123", "test@example.com")
            rr := httptest.NewRecorder()

            HandlerName(rr, req)

            require.Equal(t, http.StatusCreated, rr.Code)

            var response map[string]interface{}
            err := json.Unmarshal(rr.Body.Bytes(), &response)
            require.NoError(t, err)

            assert.NotEmpty(t, response["id"])
        })
    })

    t.Run("Edge Cases", func(t *testing.T) {
        t.Run("should handle very long input", func(t *testing.T) {
            body := map[string]interface{}{
                "requiredField": string(make([]byte, 10000)),
            }
            req := createRequest(http.MethodPost, "/api/resource", body, "user123", "test@example.com")
            rr := httptest.NewRecorder()

            HandlerName(rr, req)

            // Should either accept or reject gracefully (not panic)
            assert.Contains(t, []int{http.StatusCreated, http.StatusBadRequest}, rr.Code)
        })

        t.Run("should handle special characters in input", func(t *testing.T) {
            body := map[string]interface{}{
                "requiredField": "Test <script>alert('xss')</script>",
            }
            req := createRequest(http.MethodPost, "/api/resource", body, "user123", "test@example.com")
            rr := httptest.NewRecorder()

            HandlerName(rr, req)

            // Should handle without crashing
            assert.NotEqual(t, http.StatusInternalServerError, rr.Code)
        })
    })
}

When to Use

  • When creating new screens in frontend/src/screens/
  • When creating new components in frontend/src/components/
  • When creating new handlers in backend/internal/handlers/
  • When adding coverage to existing code
  • When fixing bugs (add regression tests)

Output

Generate the complete test file with:

  • All required imports and mocks
  • Tests for all states (loading, error, success)
  • State transition tests (critical!)
  • User interaction tests
  • Edge case tests

Place the generated file in the correct __tests__ directory.

Score

Total Score

60/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

Reviews

💬

Reviews coming soon