Back to Chapters

Chapter 12: Best Practices

Test Design Best Practices

Effective test design is crucial for creating maintainable, reliable, and valuable tests.

Single Responsibility Principle

Each test method should test only one thing:

import org.testng.Assert;
import org.testng.annotations.Test;

public class SingleResponsibilityExample {

    // Good: Each test method tests one specific functionality
    @Test
    public void testUserCreation() {
        User user = new User("john", "password123");
        Assert.assertEquals(user.getUsername(), "john", "Username should match");
    }

    @Test
    public void testUserAuthentication() {
        User user = new User("john", "password123");
        Assert.assertTrue(user.authenticate("password123"), "Authentication should succeed");
    }

    @Test
    public void testUserAuthenticationFailure() {
        User user = new User("john", "password123");
        Assert.assertFalse(user.authenticate("wrongpassword"), "Authentication should fail");
    }

    // Bad: Testing multiple things in one test method
    @Test
    public void testUserCreationAndAuthentication() {
        User user = new User("john", "password123");
        Assert.assertEquals(user.getUsername(), "john", "Username should match");
        Assert.assertTrue(user.authenticate("password123"), "Authentication should succeed");
        Assert.assertFalse(user.authenticate("wrongpassword"), "Authentication should fail");
    }

    // Simple User class for demonstration
    static class User {
        private String username;
        private String password;

        public User(String username, String password) {
            this.username = username;
            this.password = password;
        }

        public String getUsername() {
            return username;
        }

        public boolean authenticate(String password) {
            return this.password.equals(password);
        }
    }
}

Arrange-Act-Assert Pattern

Structure your tests using the Arrange-Act-Assert pattern:

import org.testng.Assert;
import org.testng.annotations.Test;

import java.util.ArrayList;
import java.util.List;

public class ArrangeActAssertExample {

    @Test
    public void testAddItem() {
        // Arrange
        ShoppingCart cart = new ShoppingCart();
        Item item = new Item("Book", 29.99);

        // Act
        cart.addItem(item);

        // Assert
        Assert.assertEquals(cart.getItemCount(), 1, "Cart should have 1 item");
        Assert.assertEquals(cart.getTotalPrice(), 29.99, 0.001, "Total price should be 29.99");
    }

    @Test
    public void testRemoveItem() {
        // Arrange
        ShoppingCart cart = new ShoppingCart();
        Item book = new Item("Book", 29.99);
        Item pen = new Item("Pen", 5.99);
        cart.addItem(book);
        cart.addItem(pen);

        // Act
        cart.removeItem(book);

        // Assert
        Assert.assertEquals(cart.getItemCount(), 1, "Cart should have 1 item");
        Assert.assertEquals(cart.getTotalPrice(), 5.99, 0.001, "Total price should be 5.99");
    }

    // Simple classes for demonstration
    static class Item {
        private String name;
        private double price;

        public Item(String name, double price) {
            this.name = name;
            this.price = price;
        }

        public String getName() {
            return name;
        }

        public double getPrice() {
            return price;
        }
    }

    static class ShoppingCart {
        private List<Item> items = new ArrayList<>();

        public void addItem(Item item) {
            items.add(item);
        }

        public void removeItem(Item item) {
            items.remove(item);
        }

        public int getItemCount() {
            return items.size();
        }

        public double getTotalPrice() {
            return items.stream()
                    .mapToDouble(Item::getPrice)
                    .sum();
        }
    }
}

Descriptive Test Names

Use descriptive test names that clearly indicate what is being tested:

import org.testng.Assert;
import org.testng.annotations.Test;

public class DescriptiveTestNamesExample {

    // Good: Descriptive test names
    @Test
    public void userShouldBeAbleToLoginWithValidCredentials() {
        // Test implementation
        boolean loginResult = login("validUser", "validPassword");
        Assert.assertTrue(loginResult, "Login should succeed with valid credentials");
    }

    @Test
    public void userShouldNotBeAbleToLoginWithInvalidPassword() {
        // Test implementation
        boolean loginResult = login("validUser", "invalidPassword");
        Assert.assertFalse(loginResult, "Login should fail with invalid password");
    }

    @Test
    public void userShouldNotBeAbleToLoginWithNonExistentUsername() {
        // Test implementation
        boolean loginResult = login("nonExistentUser", "anyPassword");
        Assert.assertFalse(loginResult, "Login should fail with non-existent username");
    }

    // Bad: Vague test names
    @Test
    public void testLogin1() {
        // Test implementation
        boolean loginResult = login("validUser", "validPassword");
        Assert.assertTrue(loginResult);
    }

    @Test
    public void testLogin2() {
        // Test implementation
        boolean loginResult = login("validUser", "invalidPassword");
        Assert.assertFalse(loginResult);
    }

    // Simple login method for demonstration
    private boolean login(String username, String password) {
        return "validUser".equals(username) && "validPassword".equals(password);
    }
}

Independent Tests

Tests should be independent of each other:

import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import java.util.ArrayList;
import java.util.List;

public class IndependentTestsExample {

    private List<String> testList;

    @BeforeMethod
    public void setup() {
        // Reset the test data before each test
        testList = new ArrayList<>();
    }

    @Test
    public void testAddItem() {
        testList.add("Item 1");
        Assert.assertEquals(testList.size(), 1, "List should have 1 item");
    }

    @Test
    public void testRemoveItem() {
        testList.add("Item 1");
        testList.remove("Item 1");
        Assert.assertEquals(testList.size(), 0, "List should be empty");
    }

    @Test
    public void testClearList() {
        testList.add("Item 1");
        testList.add("Item 2");
        testList.clear();
        Assert.assertEquals(testList.size(), 0, "List should be empty");
    }

    // Bad: Dependent tests
    /*
    private static List<String> sharedList = new ArrayList<>();

    @Test(priority = 1)
    public void testAddItem() {
        sharedList.add("Item 1");
        Assert.assertEquals(sharedList.size(), 1, "List should have 1 item");
    }

    @Test(priority = 2)
    public void testRemoveItem() {
        // This test depends on testAddItem adding an item
        sharedList.remove("Item 1");
        Assert.assertEquals(sharedList.size(), 0, "List should be empty");
    }
    */
}

Appropriate Assertions

Use the most appropriate assertions for your tests:

import org.testng.Assert;
import org.testng.annotations.Test;

import java.util.Arrays;
import java.util.List;

public class AppropriateAssertionsExample {

    @Test
    public void testEquality() {
        // Testing equality
        String actual = "Hello, World!";
        String expected = "Hello, World!";
        Assert.assertEquals(actual, expected, "Strings should be equal");
    }

    @Test
    public void testBoolean() {
        // Testing boolean conditions
        boolean condition = 5 > 3;
        Assert.assertTrue(condition, "5 should be greater than 3");

        condition = 5 < 3;
        Assert.assertFalse(condition, "5 should not be less than 3");
    }

    @Test
    public void testNull() {
        // Testing null values
        String nullString = null;
        Assert.assertNull(nullString, "String should be null");

        String nonNullString = "Not null";
        Assert.assertNotNull(nonNullString, "String should not be null");
    }

    @Test
    public void testCollections() {
        // Testing collections
        List<String> actual = Arrays.asList("Apple", "Banana", "Cherry");
        List<String> expected = Arrays.asList("Apple", "Banana", "Cherry");
        Assert.assertEquals(actual, expected, "Lists should be equal");
    }

    @Test
    public void testExceptions() {
        // Testing exceptions
        try {
            int result = 10 / 0; // This should throw ArithmeticException
            Assert.fail("Expected ArithmeticException was not thrown");
        } catch (ArithmeticException e) {
            // Expected exception was thrown
            Assert.assertEquals(e.getMessage(), "/ by zero", "Exception message should match");
        }
    }
}

Test Organization Best Practices

Organizing your tests properly makes them easier to maintain and understand.

Group Related Tests

Group related tests together using TestNG's grouping feature:

import org.testng.annotations.Test;

public class GroupingExample {

    @Test(groups = {"login", "smoke"})
    public void testValidLogin() {
        // Test implementation
    }

    @Test(groups = {"login", "regression"})
    public void testInvalidLogin() {
        // Test implementation
    }

    @Test(groups = {"registration", "smoke"})
    public void testValidRegistration() {
        // Test implementation
    }

    @Test(groups = {"registration", "regression"})
    public void testInvalidRegistration() {
        // Test implementation
    }

    @Test(groups = {"profile", "smoke"})
    public void testViewProfile() {
        // Test implementation
    }

    @Test(groups = {"profile", "regression"})
    public void testUpdateProfile() {
        // Test implementation
    }
}

Use Test Suites

Organize your tests into suites using TestNG XML files:

<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="TestSuite">
    <test name="LoginTests">
        <classes>
            <class name="com.example.LoginTest"/>
        </classes>
    </test>

    <test name="RegistrationTests">
        <classes>
            <class name="com.example.RegistrationTest"/>
        </classes>
    </test>

    <test name="ProfileTests">
        <classes>
            <class name="com.example.ProfileTest"/>
        </classes>
    </test>
</suite>

Use Packages for Organization

Organize your tests into packages based on functionality:

src/test/java/
├── com/
│   └── example/
│       ├── login/
│       │   ├── LoginTest.java
│       │   └── AuthenticationTest.java
│       ├── registration/
│       │   ├── RegistrationTest.java
│       │   └── ValidationTest.java
│       └── profile/
│           ├── ProfileViewTest.java
│           └── ProfileUpdateTest.java

Use Base Test Classes

Create base test classes to share common setup and teardown logic:

import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class BaseTest {

    protected Connection dbConnection;

    @BeforeMethod
    public void baseSetup() throws SQLException {
        // Common setup logic
        dbConnection = DriverManager.getConnection("jdbc:h2:mem:testdb", "sa", "");

        // Create test data
        createTestData();
    }

    @AfterMethod
    public void baseTearDown() throws SQLException {
        // Common teardown logic
        if (dbConnection != null && !dbConnection.isClosed()) {
            dbConnection.close();
        }
    }

    private void createTestData() throws SQLException {
        // Create common test data
    }
}

// Example of a test class extending the base test class
import org.testng.Assert;
import org.testng.annotations.Test;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class UserTest extends BaseTest {

    @Test
    public void testGetUser() throws SQLException {
        // The dbConnection is already set up by the base class
        try (PreparedStatement stmt = dbConnection.prepareStatement(
                "SELECT * FROM users WHERE id = 1")) {

            ResultSet rs = stmt.executeQuery();
            Assert.assertTrue(rs.next(), "User should exist");
            Assert.assertEquals(rs.getString("name"), "John Doe", "User name should match");
        }
    }

    @Test
    public void testCreateUser() throws SQLException {
        // The dbConnection is already set up by the base class
        try (PreparedStatement stmt = dbConnection.prepareStatement(
                "INSERT INTO users (id, name, email) VALUES (?, ?, ?)")) {

            stmt.setInt(1, 2);
            stmt.setString(2, "Jane Smith");
            stmt.setString(3, "jane@example.com");

            int rowsAffected = stmt.executeUpdate();
            Assert.assertEquals(rowsAffected, 1, "One row should be inserted");
        }
    }
}

Test Data Management Best Practices

Proper test data management is essential for reliable and maintainable tests.

Use Data Providers

Use data providers to test multiple scenarios:

import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

public class DataProviderExample {

    @DataProvider(name = "loginData")
    public Object[][] createLoginData() {
        return new Object[][] {
            {"validUser", "validPassword", true, "Valid credentials should succeed"},
            {"validUser", "invalidPassword", false, "Invalid password should fail"},
            {"invalidUser", "validPassword", false, "Invalid username should fail"},
            {"", "validPassword", false, "Empty username should fail"},
            {"validUser", "", false, "Empty password should fail"},
            {null, "validPassword", false, "Null username should fail"},
            {"validUser", null, false, "Null password should fail"}
        };
    }

    @Test(dataProvider = "loginData")
    public void testLogin(String username, String password, boolean expectedResult, String message) {
        boolean actualResult = login(username, password);
        Assert.assertEquals(actualResult, expectedResult, message);
    }

    // Simple login method for demonstration
    private boolean login(String username, String password) {
        if (username == null || password == null) {
            return false;
        }
        return "validUser".equals(username) && "validPassword".equals(password);
    }
}