Cheat SheetsJava A–ZTesting & Best Practices

Testing & Best Practices — Cheat Sheet

Java A–Z · 6 topics. Download the PDF or the Instagram carousel and share it.

Cheat Sheet · AiCanCode.org
Testing & Best Practices
Java A–Z6 topicsQuick revision reference
1

JUnit 5 Testing

  • @BeforeEach / @AfterEach run per test method; @BeforeAll / @AfterAll run once per class.
  • @ParameterizedTest with @CsvSource or @MethodSource eliminates repetitive test methods.
  • assertAll() runs all assertions even if earlier ones fail — better for compound checks.
  • @Nested classes group related tests and can have their own @BeforeEach setup.
  • AssertJ provides fluent assertions with far better error messages than raw JUnit assertions.
CalculatorTest.java
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

class CalculatorTest {

    private Calculator calc;

    @BeforeEach
    void setUp() {
        calc = new Calculator();  // fresh instance per test
    }

    @AfterEach
    void tearDown() {
        // cleanup if needed
    }

    @Test
    @DisplayName("Adding two positive numbers returns their sum")
    void addPositiveNumbers() {
        assertEquals(5, calc.add(2, 3));
    }

    @Test
    void divideByZeroThrows() {
        assertThrows(ArithmeticException.class,
            () -> calc.divide(10, 0));
    }

    @Test
    void multipleAssertions() {
        assertAll("calculator",
            () -> assertEquals(4,  calc.add(2, 2)),
            () -> assertEquals(0,  calc.subtract(5, 5)),
            () -> assertEquals(6,  calc.multiply(2, 3))
        );  // all run even if one fails
    }

    @Test
    @Disabled("Calculator.sqrt not implemented yet")
    void sqrt() { /* ... */ }
}
2

Mockito and Test Doubles

  • @Mock creates a mock; @InjectMocks creates the class under test and injects mocks.
  • when().thenReturn() stubs; verify() asserts interactions; ArgumentCaptor captures arguments.
  • Spies wrap real objects — real methods run by default, selective stubbing possible.
  • Use doReturn/doThrow for void methods and spies to avoid calling the real method.
  • Strict stubbing (MockitoExtension default) fails on unused stubs — keeps tests clean.
MockitoBasics.java
import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;
import org.junit.jupiter.api.extension.ExtendWith;

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    UserRepository repo;        // auto-created mock

    @Mock
    EmailService emailService;

    @InjectMocks
    UserService service;        // repo + emailService injected automatically

    @Test
    void findByIdReturnsUser() {
        // Stub: when repo.findById(42) is called, return this user
        User alice = new User(42, "Alice", "alice@example.com");
        when(repo.findById(42)).thenReturn(Optional.of(alice));

        User result = service.findById(42);

        assertThat(result.getName()).isEqualTo("Alice");
    }

    @Test
    void findByIdThrowsWhenNotFound() {
        when(repo.findById(anyInt())).thenReturn(Optional.empty());

        assertThatThrownBy(() -> service.findById(99))
            .isInstanceOf(UserNotFoundException.class);
    }
}
3

Java Security

  • Use PreparedStatement always — SQL injection is the #1 Java vulnerability.
  • Validate and normalise file paths before use to prevent path traversal.
  • Use PBKDF2, bcrypt, or Argon2 for passwords — never plain SHA or MD5.
  • SecureRandom is the only acceptable random source for security tokens and salts.
  • Never trust user input, never hard-code secrets, always apply least privilege.
Vulnerabilities.java
// SQL INJECTION — NEVER do this
String user = request.getParameter("user");
String sql = "SELECT * FROM users WHERE name = '" + user + "'";
// user = "'; DROP TABLE users; --" → catastrophic

// SAFE — PreparedStatement
PreparedStatement ps = conn.prepareStatement(
    "SELECT * FROM users WHERE name = ?");
ps.setString(1, user); // parameterised — injection impossible

// PATH TRAVERSAL — validate file paths
String filename = request.getParameter("file");
// BAD: Files.readString(Path.of("/uploads/" + filename))
// filename = "../../../etc/passwd" → reads sensitive file

// SAFE — normalise and validate
Path base = Path.of("/uploads").toRealPath();
Path requested = base.resolve(filename).normalize().toRealPath();
if (!requested.startsWith(base)) {
    throw new SecurityException("Path traversal detected");
}

// DESERIALIZATION — whitelist allowed classes
ObjectInputStream ois = new ObjectInputStream(fis);
ois.setObjectInputFilter(
    ObjectInputFilter.Config.createFilter(
        "com.example.dto.*;java.util.*;!*")); // only allow safe classes
4

Logging in Java

  • Use SLF4J as the logging facade — never import Logback/Log4j2 classes in application code.
  • Parameterised logging ({}) avoids string concatenation when the level is disabled.
  • Always log exceptions as the last argument to include the full stack trace.
  • MDC adds request-scoped context (traceId, userId) to every log line — clear it in finally.
  • Use async appenders in production to prevent slow I/O from blocking application threads.
SLF4JBasics.java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class OrderService {
    // One logger per class — static final
    private static final Logger log =
        LoggerFactory.getLogger(OrderService.class);

    public Order placeOrder(OrderRequest req) {
        log.debug("Placing order for user={} item={}", req.getUserId(), req.getItemId());

        try {
            Order order = processOrder(req);
            log.info("Order placed orderId={} userId={} amount={}",
                order.getId(), req.getUserId(), order.getAmount());
            return order;
        } catch (InsufficientStockException e) {
            log.warn("Stock insufficient for item={} requested={}",
                req.getItemId(), req.getQuantity());
            throw e;
        } catch (Exception e) {
            // Always log exception with message — not just e.getMessage()
            log.error("Failed to place order for user={}", req.getUserId(), e);
            throw new OrderException("Order processing failed", e);
        }
    }
}
5

Java Best Practices

  • Make classes and members as private as possible — expand access only when required.
  • Return empty collections (List.of()) instead of null — prevents NullPointerException.
  • Prefer composition over inheritance for code reuse — inheritance is for true IS-A relationships.
  • Optional is for return types only — not fields, parameters, or collection elements.
  • Exceptions must include enough context to diagnose without needing to reproduce the bug.
APIDesign.java
// BAD — public mutable fields, no validation
public class Range {
    public int start;
    public int end;
}

// GOOD — encapsulated, validated, immutable, fluent factory
public final class Range {
    private final int start;
    private final int end;

    private Range(int start, int end) {
        if (start > end) throw new IllegalArgumentException(
            "start (%d) must be <= end (%d)".formatted(start, end));
        this.start = start;
        this.end   = end;
    }

    public static Range of(int start, int end) {
        return new Range(start, end);
    }

    public int start() { return start; }
    public int end()   { return end; }
    public int length()  { return end - start; }
    public boolean contains(int value) {
        return value >= start && value < end;
    }

    // Return empty collection, not null
    public List<Integer> toList() {
        if (start >= end) return List.of();
        return IntStream.range(start, end).boxed().collect(Collectors.toList());
    }
}
6

Performance and Profiling

  • Measure first — profiler before optimiser. Guessing at bottlenecks wastes time.
  • JMH is the only reliable Java micro-benchmark tool — handles warm-up and dead code elimination.
  • async-profiler provides accurate CPU/allocation/lock flame graphs with minimal overhead.
  • Algorithm and data structure choices have far more impact than micro-optimisations.
  • Common traps: autoboxing in loops, regex per call, N+1 queries, excessive logging in hot paths.
JMHBenchmark.java
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
import java.util.concurrent.TimeUnit;

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
@Fork(value = 2, warmups = 1)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 10, time = 1)
public class StringBenchmark {

    @Param({"10", "100", "1000"})
    private int size;

    private String[] data;

    @Setup
    public void setUp() {
        data = IntStream.range(0, size)
            .mapToObj(Integer::toString)
            .toArray(String[]::new);
    }

    @Benchmark
    public void concatenationPlus(Blackhole bh) {
        String result = "";
        for (String s : data) result += s;
        bh.consume(result); // prevents dead code elimination
    }

    @Benchmark
    public void stringBuilder(Blackhole bh) {
        StringBuilder sb = new StringBuilder();
        for (String s : data) sb.append(s);
        bh.consume(sb.toString());
    }
}
// Run: java -jar benchmarks.jar StringBenchmark
Learn this free with Aria, your AI tutor → AiCanCode.org/learn/java