Software Testing

Contents

Unit Testing

Jest Example

// calculator.ts export class Calculator { add(a: number, b: number): number { return a + b; } subtract(a: number, b: number): number { return a - b; } multiply(a: number, b: number): number { return a * b; } divide(a: number, b: number): number { if (b === 0) { throw new Error('Division by zero'); } return a / b; } } // calculator.test.ts import { Calculator } from './calculator'; describe('Calculator', () => { let calculator: Calculator; beforeEach(() => { calculator = new Calculator(); }); describe('add', () => { it('should add two positive numbers', () => { expect(calculator.add(2, 3)).toBe(5); }); it('should handle negative numbers', () => { expect(calculator.add(-2, 3)).toBe(1); expect(calculator.add(2, -3)).toBe(-1); expect(calculator.add(-2, -3)).toBe(-5); }); }); describe('divide', () => { it('should divide two numbers', () => { expect(calculator.divide(6, 2)).toBe(3); }); it('should throw error on division by zero', () => { expect(() => calculator.divide(6, 0)).toThrow('Division by zero'); }); }); });

JUnit Example

// Java JUnit Test import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; public class UserServiceTest { private UserService userService; private UserRepository mockRepository; @BeforeEach void setUp() { mockRepository = mock(UserRepository.class); userService = new UserService(mockRepository); } @Test void createUser_WithValidData_ShouldSucceed() { // Arrange User user = new User("John", "john@example.com"); when(mockRepository.save(any(User.class))).thenReturn(user); // Act User result = userService.createUser(user); // Assert assertNotNull(result); assertEquals("John", result.getName()); assertEquals("john@example.com", result.getEmail()); verify(mockRepository).save(any(User.class)); } @Test void createUser_WithInvalidEmail_ShouldThrowException() { // Arrange User user = new User("John", "invalid-email"); // Act & Assert assertThrows(ValidationException.class, () -> userService.createUser(user)); } }

Integration Testing

Spring Boot Integration Test

@SpringBootTest @AutoConfigureMockMvc public class UserControllerIntegrationTest { @Autowired private MockMvc mockMvc; @Autowired private ObjectMapper objectMapper; @Autowired private UserRepository userRepository; @BeforeEach void setUp() { userRepository.deleteAll(); } @Test void createUser_ShouldReturnCreatedUser() throws Exception { // Arrange UserDTO userDTO = new UserDTO("John", "john@example.com"); String json = objectMapper.writeValueAsString(userDTO); // Act & Assert mockMvc.perform(post("/api/users") .contentType(MediaType.APPLICATION_JSON) .content(json)) .andExpect(status().isCreated()) .andExpect(jsonPath("$.name").value("John")) .andExpect(jsonPath("$.email").value("john@example.com")); // Verify database state Optional savedUser = userRepository.findByEmail("john@example.com"); assertTrue(savedUser.isPresent()); assertEquals("John", savedUser.get().getName()); } }

Database Integration Test

@DataJpaTest public class UserRepositoryIntegrationTest { @Autowired private UserRepository userRepository; @Test void findByEmail_ShouldReturnUser() { // Arrange User user = new User("John", "john@example.com"); userRepository.save(user); // Act Optional result = userRepository.findByEmail("john@example.com"); // Assert assertTrue(result.isPresent()); assertEquals("John", result.get().getName()); } @Test void save_WithDuplicateEmail_ShouldThrowException() { // Arrange User user1 = new User("John", "john@example.com"); User user2 = new User("Jane", "john@example.com"); userRepository.save(user1); // Act & Assert assertThrows(DataIntegrityViolationException.class, () -> userRepository.save(user2)); } }

End-to-End Testing

Cypress Example

// cypress/integration/login.spec.ts describe('Login Flow', () => { beforeEach(() => { cy.visit('/login'); }); it('should login successfully with valid credentials', () => { // Enter credentials cy.get('[data-test="email-input"]') .type('user@example.com'); cy.get('[data-test="password-input"]') .type('password123'); // Submit form cy.get('[data-test="login-button"]').click(); // Assert successful login cy.url().should('include', '/dashboard'); cy.get('[data-test="welcome-message"]') .should('contain', 'Welcome, User'); }); it('should show error with invalid credentials', () => { // Enter invalid credentials cy.get('[data-test="email-input"]') .type('invalid@example.com'); cy.get('[data-test="password-input"]') .type('wrongpassword'); // Submit form cy.get('[data-test="login-button"]').click(); // Assert error message cy.get('[data-test="error-message"]') .should('be.visible') .and('contain', 'Invalid credentials'); }); });

Selenium Example

// Selenium WebDriver Test public class LoginTest { private WebDriver driver; private String baseUrl; @BeforeClass public void setUp() { driver = new ChromeDriver(); baseUrl = "https://example.com"; driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS); } @Test public void testLoginSuccess() { // Navigate to login page driver.get(baseUrl + "/login"); // Enter credentials WebElement emailInput = driver.findElement(By.id("email")); WebElement passwordInput = driver.findElement(By.id("password")); WebElement loginButton = driver.findElement(By.id("login-btn")); emailInput.sendKeys("user@example.com"); passwordInput.sendKeys("password123"); loginButton.click(); // Verify successful login WebElement welcomeMessage = driver.findElement( By.className("welcome-message") ); assertTrue(welcomeMessage.isDisplayed()); assertEquals("Welcome, User", welcomeMessage.getText()); } @AfterClass public void tearDown() { if (driver != null) { driver.quit(); } } }

Test-Driven Development

TDD Process

  1. Write a failing test
  2. Write minimal code to pass the test
  3. Refactor the code
  4. Repeat

TDD Example

// Step 1: Write failing test describe('StringCalculator', () => { it('should return 0 for empty string', () => { const calculator = new StringCalculator(); expect(calculator.add("")).toBe(0); }); }); // Step 2: Write minimal code to pass class StringCalculator { add(numbers: string): number { if (numbers === "") { return 0; } return 0; // Still need to implement actual adding } } // Step 3: Write next failing test it('should return number for single number', () => { const calculator = new StringCalculator(); expect(calculator.add("1")).toBe(1); }); // Step 4: Update code to pass class StringCalculator { add(numbers: string): number { if (numbers === "") { return 0; } return parseInt(numbers); } }

Mocking and Stubbing

Jest Mocking

// userService.test.ts import { UserService } from './userService'; import { UserRepository } from './userRepository'; jest.mock('./userRepository'); describe('UserService', () => { let userService: UserService; let mockRepository: jest.Mocked; beforeEach(() => { mockRepository = new UserRepository() as jest.Mocked; userService = new UserService(mockRepository); }); it('should get user by id', async () => { // Arrange const mockUser = { id: 1, name: 'John' }; mockRepository.findById.mockResolvedValue(mockUser); // Act const result = await userService.getUserById(1); // Assert expect(result).toEqual(mockUser); expect(mockRepository.findById).toHaveBeenCalledWith(1); }); });

Mockito Example

// Java Mockito Test @ExtendWith(MockitoExtension.class) public class UserServiceTest { @Mock private UserRepository userRepository; @InjectMocks private UserService userService; @Test void getUserById_ShouldReturnUser() { // Arrange User mockUser = new User(1L, "John"); when(userRepository.findById(1L)) .thenReturn(Optional.of(mockUser)); // Act User result = userService.getUserById(1L); // Assert assertNotNull(result); assertEquals("John", result.getName()); verify(userRepository).findById(1L); } }

Test Automation

GitHub Actions Example

# .github/workflows/test.yml name: Run Tests on: push: branches: [ main ] pull_request: branches: [ main ] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Node.js uses: actions/setup-node@v2 with: node-version: '16' - name: Install dependencies run: npm ci - name: Run unit tests run: npm run test - name: Run integration tests run: npm run test:integration - name: Run E2E tests run: npm run test:e2e - name: Upload coverage uses: actions/upload-artifact@v2 with: name: coverage path: coverage/