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);
}
}