As a seasoned Java software engineer, writing code is only half the job — testing, logging, and debugging are equally crucial to building production-grade systems. Whether you’re writing a simple REST API or orchestrating microservices, robust testing and clear logs can be the difference between a smooth deploy and a 3 AM incident.
In this post, we’ll cover:
- ✅ Writing unit & integration tests using JUnit 5
- ✅ Creating behavior-driven tests with Cucumber
- ✅ Adding meaningful logging with SLF4J + Logback
- ✅ Debugging effectively in modern Java projects
🧪 1. Unit Testing with JUnit 5
🛠️ Why Use JUnit?
JUnit is the standard unit testing framework for Java. It’s fast, supports parameterized tests, and integrates with all major build tools (Maven/Gradle) and CI platforms (GitHub Actions, Jenkins).
📄 Sample Test
class Calculator {
public int add(int a, int b) {
return a + b;
}
}
class CalculatorTest {
private Calculator calculator;
@BeforeEach
void setup() {
calculator = new Calculator();
}
@Test
void shouldAddTwoNumbers() {
Assertions.assertEquals(5, calculator.add(2, 3));
}
}
🧠 Pro Tips:
- Use
@BeforeEach
for setup logic - Structure tests into
given/when/then
comments - Use
AssertJ
for better readability:assertThat(result).isEqualTo(5);
🌱 2. Behavior-Driven Testing with Cucumber
Cucumber lets you write tests in plain English (.feature
files), making tests readable by QA, PMs, and non-dev stakeholders. It’s great for acceptance testing and complex business logic.
📄 Sample Feature File
Feature: Addition
Scenario: Add two numbers
Given the numbers 4 and 6
When I add them
Then the result should be 10
🧪 Step Definitions
public class StepDefinitions {
private int a, b, result;
@Given("the numbers {int} and {int}")
public void givenNumbers(int num1, int num2) {
a = num1;
b = num2;
}
@When("I add them")
public void addThem() {
result = a + b;
}
@Then("the result should be {int}")
public void verifyResult(int expected) {
Assertions.assertEquals(expected, result);
}
}
🧠 Pro Tips:
- Use Cucumber only when business logic benefits from natural-language testing
- Keep step definitions clean and reusable
- Run Cucumber tests with Maven/Gradle plugins or JUnit runners
📄 3. Logging Best Practices (SLF4J + Logback)
Logging is vital for understanding app behavior in dev and production.
✅ Use SLF4J for Logging
It’s the standard facade for Java logging frameworks.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class UserService {
private static final Logger logger = LoggerFactory.getLogger(UserService.class);
public void registerUser(String username) {
logger.info("Registering user: {}", username);
// do something
logger.debug("Completed DB transaction for user: {}", username);
}
}
🧠 Pro Tips:
- Use
info
for high-level actions,debug
for dev-only details,warn/error
for issues - Avoid string concatenation (
"User: " + name
) — use{}
placeholders - Configure Logback via
logback-spring.xml
for log levels, formats, rotation
🐞 4. Debugging Like a Pro
Modern IDEs like IntelliJ make debugging easier than ever.
🧰 How to Debug Java Efficiently:
- Use breakpoints and run in debug mode
- Set conditional breakpoints for filtering (
userId == 5
) - Use Evaluate Expression to inspect or test code changes live
- Add logpoints (non-intrusive logs during debugging)
- Analyze stack traces and thread dumps for concurrency issues
🔥 Pro Debugging Tips:
- Use
@Slf4j
from Lombok for cleaner logging injection - For integration debugging, use Postman + logs + breakpoints
- For Spring Boot, enable:
logging.level.org.springframework=DEBUG
✅ Final Thoughts
Testing, logging, and debugging are not afterthoughts — they are core engineering skills that separate average devs from professionals.
- Write unit tests with JUnit for core logic
- Use Cucumber where business logic needs to be verified in plain language
- Keep your logs meaningful and consistent
- Debug with intention and tools — not print statements
Master these practices, and you’ll not only ship fewer bugs — you’ll also debug and fix them faster when they inevitably arise.
Leave a Reply