Cara Pakai GitHub Copilot untuk Nulis Unit Test Otomatis
ID | EN

Cara Pakai GitHub Copilot untuk Nulis Unit Test Otomatis

Selasa, 24 Des 2024

Jujur aja, nulis unit test itu… males. Kita semua tahu testing itu penting, tapi waktu deadline mepet, test sering jadi korban pertama yang di-skip. Nah, di sinilah GitHub Copilot bisa jadi game changer.

Kenapa Sih Testing Itu Penting (Tapi Sering Dilewatin)?

Kita semua pernah di posisi ini: fitur udah jalan, deadline tinggal beberapa jam, dan pilihan antara nulis test atau push dulu terus “nanti aja testnya”. Spoiler: “nanti” itu biasanya nggak pernah datang.

Padahal, unit test itu kayak asuransi buat code kita:

  • Prevent regression - Kalau ada yang break, kita tahu duluan sebelum user complain
  • Documentation - Test yang bagus bisa jadi dokumentasi behavior code
  • Refactor dengan tenang - Mau ubah implementasi? Tinggal jalanin test

Masalahnya, nulis test itu repetitif dan time-consuming. Di sinilah AI bisa bantu.

Apa Itu GitHub Copilot?

GitHub Copilot adalah AI pair programmer yang dibuat oleh GitHub dan OpenAI. Dia bisa suggest code, autocomplete, bahkan generate whole functions berdasarkan context dan comments yang kita tulis.

Setup GitHub Copilot

  1. Subscribe ke GitHub Copilot - Bisa lewat github.com/features/copilot. Ada free trial 30 hari.

  2. Install extension di VS Code:

    • Buka VS Code
    • Pergi ke Extensions (Ctrl+Shift+X)
    • Search “GitHub Copilot”
    • Install dan sign in dengan GitHub account
  3. Verify instalasi - Coba ketik comment di file JavaScript, harusnya Copilot mulai kasih suggestions.

Copilot untuk Generate Test - Basic Example

Oke, let’s get practical. Misalnya kita punya function sederhana:

// utils/math.js
export function calculateDiscount(price, discountPercent) {
  if (discountPercent < 0 || discountPercent > 100) {
    throw new Error('Invalid discount percentage');
  }
  return price - (price * discountPercent / 100);
}

Sekarang buat file test-nya. Yang perlu kamu lakukan cuma:

// utils/math.test.js
import { calculateDiscount } from './math';

// Test calculateDiscount function

Begitu kamu ketik comment itu dan pencet Enter, Copilot biasanya langsung suggest:

describe('calculateDiscount', () => {
  test('should calculate discount correctly', () => {
    expect(calculateDiscount(100, 20)).toBe(80);
  });

  test('should return original price when discount is 0', () => {
    expect(calculateDiscount(100, 0)).toBe(100);
  });

  test('should throw error for negative discount', () => {
    expect(() => calculateDiscount(100, -10)).toThrow('Invalid discount percentage');
  });

  test('should throw error for discount over 100', () => {
    expect(() => calculateDiscount(100, 150)).toThrow('Invalid discount percentage');
  });
});

Lumayan kan? Dari satu comment, kita dapet 4 test cases yang reasonable.

Generate Test dari Function Signature

Trick yang lebih powerful: copy-paste function signature atau bahkan seluruh function ke file test, terus tambahin comment yang descriptive.

// Paste function yang mau di-test di comment
/*
function validateEmail(email) {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email);
}
*/

// Generate comprehensive tests for validateEmail
describe('validateEmail', () => {

Copilot bakal “baca” context dari comment dan generate tests yang lebih comprehensive:

describe('validateEmail', () => {
  test('should return true for valid email', () => {
    expect(validateEmail('test@example.com')).toBe(true);
  });

  test('should return true for email with subdomain', () => {
    expect(validateEmail('test@mail.example.com')).toBe(true);
  });

  test('should return false for email without @', () => {
    expect(validateEmail('testexample.com')).toBe(false);
  });

  test('should return false for email without domain', () => {
    expect(validateEmail('test@')).toBe(false);
  });

  test('should return false for email with spaces', () => {
    expect(validateEmail('test @example.com')).toBe(false);
  });

  test('should return false for empty string', () => {
    expect(validateEmail('')).toBe(false);
  });
});

Testing Edge Cases dengan Copilot

Edge cases adalah tempat di mana bugs suka bersembunyi. Copilot cukup bagus buat identify edge cases kalau kita kasih hint yang tepat.

Coba prompt seperti ini:

// Test edge cases for calculateDiscount
// - boundary values (0, 100)
// - floating point numbers
// - very large numbers
// - null/undefined inputs

Copilot bakal generate:

describe('calculateDiscount edge cases', () => {
  test('should handle boundary value 0%', () => {
    expect(calculateDiscount(100, 0)).toBe(100);
  });

  test('should handle boundary value 100%', () => {
    expect(calculateDiscount(100, 100)).toBe(0);
  });

  test('should handle floating point discount', () => {
    expect(calculateDiscount(100, 33.33)).toBeCloseTo(66.67);
  });

  test('should handle very large price', () => {
    expect(calculateDiscount(1000000000, 10)).toBe(900000000);
  });

  test('should handle null price', () => {
    expect(calculateDiscount(null, 10)).toBeNaN();
  });

  test('should handle undefined discount', () => {
    expect(() => calculateDiscount(100, undefined)).toThrow();
  });
});

Jest + React Testing Library Examples

Untuk React components, Copilot juga bisa bantu. Ini contoh component sederhana:

// components/Button.jsx
export function Button({ label, onClick, disabled = false, variant = 'primary' }) {
  return (
    <button
      className={`btn btn-${variant}`}
      onClick={onClick}
      disabled={disabled}
    >
      {label}
    </button>
  );
}

Di file test, tulis:

// components/Button.test.jsx
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';

// Test Button component with all props

Copilot akan suggest:

describe('Button', () => {
  test('renders button with label', () => {
    render(<Button label="Click me" onClick={() => {}} />);
    expect(screen.getByText('Click me')).toBeInTheDocument();
  });

  test('calls onClick when clicked', () => {
    const handleClick = jest.fn();
    render(<Button label="Click me" onClick={handleClick} />);
    fireEvent.click(screen.getByText('Click me'));
    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  test('is disabled when disabled prop is true', () => {
    render(<Button label="Click me" onClick={() => {}} disabled />);
    expect(screen.getByText('Click me')).toBeDisabled();
  });

  test('applies correct variant class', () => {
    render(<Button label="Click me" onClick={() => {}} variant="secondary" />);
    expect(screen.getByText('Click me')).toHaveClass('btn-secondary');
  });

  test('does not call onClick when disabled', () => {
    const handleClick = jest.fn();
    render(<Button label="Click me" onClick={handleClick} disabled />);
    fireEvent.click(screen.getByText('Click me'));
    expect(handleClick).not.toHaveBeenCalled();
  });
});

Tips Prompting Copilot untuk Test yang Lebih Baik

Copilot itu kayak junior developer - dia butuh instruksi yang jelas. Berikut tips prompting yang efektif:

1. Be Specific dengan Test Scenarios

// ❌ Kurang spesifik
// Test this function

// ✅ Lebih baik
// Test fetchUserData:
// - successful API response with user data
// - API returns 404 error
// - API returns 500 error
// - network timeout
// - malformed JSON response

2. Mention Testing Patterns

// Test using AAA pattern (Arrange, Act, Assert)
// Mock fetch API using jest.fn()

3. Specify Expected Behavior

// Test that formatCurrency:
// - adds thousand separators
// - adds 'Rp' prefix
// - handles negative numbers with parentheses
// - rounds to 2 decimal places

4. Include Edge Cases dalam Comment

// Include tests for:
// - empty array
// - single element
// - duplicate values
// - null/undefined
// - very large arrays (performance)

Contoh Real-World: Testing API Hooks

Ini contoh yang lebih real-world - testing custom hook untuk fetch data:

// hooks/useUser.js
import { useState, useEffect } from 'react';

export function useUser(userId) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function fetchUser() {
      try {
        setLoading(true);
        const response = await fetch(`/api/users/${userId}`);
        if (!response.ok) throw new Error('Failed to fetch');
        const data = await response.json();
        setUser(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    }
    fetchUser();
  }, [userId]);

  return { user, loading, error };
}

Prompt untuk Copilot:

// hooks/useUser.test.js
import { renderHook, waitFor } from '@testing-library/react';
import { useUser } from './useUser';

// Mock fetch API
global.fetch = jest.fn();

// Test useUser hook:
// - initial loading state
// - successful data fetch
// - API error handling
// - userId change triggers refetch

Copilot generates:

describe('useUser', () => {
  beforeEach(() => {
    fetch.mockClear();
  });

  test('should start with loading state', () => {
    fetch.mockImplementation(() => new Promise(() => {}));
    const { result } = renderHook(() => useUser('123'));
    
    expect(result.current.loading).toBe(true);
    expect(result.current.user).toBeNull();
    expect(result.current.error).toBeNull();
  });

  test('should return user data on successful fetch', async () => {
    const mockUser = { id: '123', name: 'John Doe' };
    fetch.mockResolvedValueOnce({
      ok: true,
      json: async () => mockUser,
    });

    const { result } = renderHook(() => useUser('123'));

    await waitFor(() => {
      expect(result.current.loading).toBe(false);
    });

    expect(result.current.user).toEqual(mockUser);
    expect(result.current.error).toBeNull();
  });

  test('should handle API error', async () => {
    fetch.mockResolvedValueOnce({
      ok: false,
    });

    const { result } = renderHook(() => useUser('123'));

    await waitFor(() => {
      expect(result.current.loading).toBe(false);
    });

    expect(result.current.user).toBeNull();
    expect(result.current.error).toBe('Failed to fetch');
  });

  test('should refetch when userId changes', async () => {
    const mockUser1 = { id: '123', name: 'John' };
    const mockUser2 = { id: '456', name: 'Jane' };

    fetch
      .mockResolvedValueOnce({ ok: true, json: async () => mockUser1 })
      .mockResolvedValueOnce({ ok: true, json: async () => mockUser2 });

    const { result, rerender } = renderHook(
      ({ userId }) => useUser(userId),
      { initialProps: { userId: '123' } }
    );

    await waitFor(() => expect(result.current.user).toEqual(mockUser1));

    rerender({ userId: '456' });

    await waitFor(() => expect(result.current.user).toEqual(mockUser2));
    expect(fetch).toHaveBeenCalledTimes(2);
  });
});

Testing Utilities

Utility functions biasanya paling gampang di-test dengan Copilot. Contoh:

// utils/string.js
export function slugify(text) {
  return text
    .toLowerCase()
    .trim()
    .replace(/[^\w\s-]/g, '')
    .replace(/[\s_-]+/g, '-')
    .replace(/^-+|-+$/g, '');
}
// utils/string.test.js
import { slugify } from './string';

// Comprehensive tests for slugify function
// Include: normal text, special characters, unicode, edge cases

Result:

describe('slugify', () => {
  test('converts text to lowercase', () => {
    expect(slugify('Hello World')).toBe('hello-world');
  });

  test('replaces spaces with hyphens', () => {
    expect(slugify('hello world')).toBe('hello-world');
  });

  test('removes special characters', () => {
    expect(slugify('hello@world!')).toBe('helloworld');
  });

  test('handles multiple spaces', () => {
    expect(slugify('hello   world')).toBe('hello-world');
  });

  test('trims whitespace', () => {
    expect(slugify('  hello world  ')).toBe('hello-world');
  });

  test('removes leading and trailing hyphens', () => {
    expect(slugify('-hello world-')).toBe('hello-world');
  });

  test('handles empty string', () => {
    expect(slugify('')).toBe('');
  });

  test('handles string with only special characters', () => {
    expect(slugify('!@#$%')).toBe('');
  });

  test('handles underscores', () => {
    expect(slugify('hello_world')).toBe('hello-world');
  });

  test('handles mixed separators', () => {
    expect(slugify('hello - world _ test')).toBe('hello-world-test');
  });
});

Limitations dan Kapan Harus Review Manual

Copilot memang powerful, tapi dia bukan magic. Ada beberapa hal yang perlu diperhatikan:

Kapan Copilot Struggle:

  1. Complex Business Logic - Kalau logic-nya complicated dan domain-specific, Copilot mungkin miss important scenarios.

  2. Integration Tests - Untuk tests yang involve multiple systems atau complex setup, manual review lebih penting.

  3. Security-related Tests - Jangan 100% rely on Copilot untuk security testing. Always review.

  4. Async/Race Conditions - Copilot kadang generate test yang brittle untuk async code.

Always Review:

  • Test assertions - Pastikan expect statements benar-benar test behavior yang penting
  • Mock implementations - Cek apakah mocks reflect real behavior
  • Edge cases - Copilot might miss edge cases yang specific ke business logic kamu
  • Test isolation - Pastikan tests nggak depend on each other

Red Flags:

// 🚩 Test yang terlalu simple/meaningless
test('should exist', () => {
  expect(myFunction).toBeDefined();
});

// 🚩 Test yang cuma test implementation, bukan behavior
test('should call internal method', () => {
  expect(internalMethod).toHaveBeenCalled();
});

// 🚩 Hardcoded values tanpa context
test('should return correct value', () => {
  expect(calculate(5)).toBe(25); // Kenapa 25? What's the logic?
});

Ini workflow yang gue personally pake:

  1. Write the function first - Copilot butuh context
  2. Create test file - Buat file .test.js baru
  3. Add descriptive comments - List test scenarios yang kamu mau
  4. Let Copilot generate - Accept suggestions yang make sense
  5. Review dan modify - Hapus yang nggak relevant, tambah yang missing
  6. Run tests - Pastikan semua pass
  7. Check coverage - Identify gaps dan tambah manual tests

Kesimpulan

GitHub Copilot bisa significantly speed up proses nulis unit test. Dari yang tadinya males nulis test karena repetitive, sekarang bisa generate boilerplate dalam hitungan detik.

Tapi ingat - Copilot itu tool, bukan replacement untuk critical thinking. Dia bagus untuk:

  • Generate boilerplate test structure
  • Suggest common test cases
  • Handle repetitive testing patterns
  • Speed up TDD workflow

Tapi tetap butuh human review untuk:

  • Business logic validation
  • Security testing
  • Edge cases yang domain-specific
  • Ensuring test quality

Start dengan functions yang simple, terus gradually pake Copilot untuk code yang lebih complex. Lama-lama, kamu bakal develop intuition kapan Copilot suggestions bagus dan kapan perlu adjustment.

Happy testing! 🧪