Test backend
AI operating system for product managers. 65 Claude Code skills, 7 multi-perspective review agents, a memory system. Battle-tested in real PM work.
npx -y skills add talgacapri/pm-os --skill test-backendAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Write, review, and audit backend tests for a Java codebase. Applies ISTQB CTFL v4.0 standards (black-box/white-box techniques, test pyramid, risk-based prioritization, defect taxonomy) with JUnit 5, Mockito, Spring Boot Test, and REST Assured. Triggers on "write tests for this service", "test this endpoint", "unit test", "integration test", "mock this repository", "test coverage Java", or any request to test Java backend code.
SKILL.md
19.7 KB, as published. Nobody here has run it
Backend Testing Skill
Stack: Java · JUnit 5 · Mockito · Spring Boot Test · REST Assured · Pact Standard: ISTQB CTFL v4.0.1 (ISO/IEC/IEEE 29119 aligned)
ISTQB Foundation: The Seven Testing Principles
These govern every decision below. Not ceremony — they are the reasoning behind the rules.
- Testing shows presence, not absence of defects. Green tests reduce risk; they don't prove correctness.
- Exhaustive testing is impossible. Apply risk-based prioritization (see §Risk). Don't test everything equally.
- Early testing saves time and money. Write tests before or alongside code. Use TDD for new services.
- Defects cluster together. A failing service method usually means adjacent methods have related bugs. Retest the cluster.
- Tests wear out. Rotate test data, add new edge cases each sprint, update tests when business rules change.
- Testing is context dependent. Payment processing needs higher coverage and formality than a notification preference toggle.
- Absence-of-defects fallacy. 100% unit test coverage with wrong acceptance criteria is worthless. Always validate against user story acceptance criteria.
Test Levels (ISTQB §2.2.1) Mapped to Java
| ISTQB Level | Java Implementation | Tooling |
|---|---|---|
| Component (Unit) | Single class in isolation — service, validator, mapper, utility | JUnit 5 + Mockito |
| Component Integration | Service + Repository (H2 in-memory), Service + external mock | Spring Boot Test (slice) |
| System | Full Spring context, all layers, real DB (Testcontainers) | Spring Boot Test + Testcontainers |
| System Integration | REST endpoints consumed by E2E client, external APIs | REST Assured + WireMock |
| Acceptance | Acceptance criteria from user stories, Given/When/Then | REST Assured (ATDD) |
The Test Pyramid: Where to Spend Effort
/ System Integration (REST Assured) \ ← few, slowest, highest confidence
/ System Tests (Testcontainers) \ ← moderate count, medium speed
/ Component Integration (@DataJpaTest) \ ← moderate, faster
/ Unit Tests (JUnit 5 + Mockito) \ ← most, fastest, isolated
/____________________________________________\
Allocation target: ~60% unit, ~25% integration, ~15% system/API.
Test Types (ISTQB §2.2.2)
| Type | Java Application |
|---|---|
| Functional | Does the service method do what the user story says? |
| Non-functional | Response time SLAs, throughput under load (k6/Gatling), memory under sustained requests |
| Black-box | Test via public API — method signature, REST endpoint — never internal state |
| White-box | Branch coverage in business logic (JaCoCo report) |
| Regression | Every bug fix gets a new @Test. Run full suite in CI on every PR. |
| Confirmation | After fixing a defect: re-run the failing test and verify it passes before closing the ticket. |
| Contract | API contracts between services (Pact consumer-driven) |
Testing Techniques (ISTQB §4)
Black-Box Techniques (apply to all method and endpoint tests)
Equivalence Partitioning (EP) Identify groups of inputs the method treats identically. Write one test per partition.
// TransferService.transfer(amount) — partitions:
// Valid: 0.01 to account balance
// Invalid negative: amount < 0
// Invalid zero: amount == 0
// Invalid over-balance: amount > balance
@Test void transfer_succeeds_with_valid_amount() { ... } // valid partition
@Test void transfer_fails_with_negative_amount() { ... } // invalid partition
@Test void transfer_fails_with_zero_amount() { ... } // boundary of invalid
@Test void transfer_fails_when_amount_exceeds_balance() { ... } // invalid partition
Boundary Value Analysis (BVA) For numeric and range inputs, test boundary - 1, boundary, boundary + 1 (3-value BVA).
// Max transfer limit: 10,000.00
@Test void transfer_succeeds_at_9999_99() { ... } // below boundary
@Test void transfer_succeeds_at_10000_00() { ... } // at boundary
@Test void transfer_fails_at_10000_01() { ... } // above boundary
Decision Table Testing Use when business logic has multiple condition combinations producing distinct outcomes. One test per column.
| isKycVerified | hasBalance | isAccountFrozen | Outcome |
|---------------|------------|-----------------|------------------|
| true | true | false | Allow transfer |
| true | false | false | Reject: no funds |
| true | - | true | Reject: frozen |
| false | - | - | Reject: KYC |
@Test void allows_transfer_when_kyc_verified_balance_sufficient_account_active() { ... }
@Test void rejects_transfer_when_balance_insufficient() { ... }
@Test void rejects_transfer_when_account_frozen() { ... }
@Test void rejects_transfer_when_kyc_not_verified() { ... }
State Transition Testing For state machines (transaction lifecycle, account states, KYC flow):
// Account states: PENDING → ACTIVE → FROZEN → CLOSED
// Test all valid transitions (valid transitions coverage)
@Test void account_transitions_from_pending_to_active_on_kyc_approval() { ... }
@Test void account_transitions_from_active_to_frozen_on_compliance_flag() { ... }
@Test void account_cannot_transition_from_closed_to_active() { ... }
White-Box Techniques (JaCoCo)
Statement Coverage: Every executable line in a service class runs under test. Target: 100%.
Branch Coverage: Every if/else, switch, and ternary is exercised with both outcomes. Target: ≥90%.
Run: ./gradlew test jacocoTestReport and review build/reports/jacoco/. Branch coverage subsumes statement coverage per ISTQB §4.3.2.
Experience-Based Techniques
Error Guessing: Before writing tests, list likely mistakes:
- Null returns from repository when record not found (NullPointerException vs Optional.empty)
- Integer overflow in currency arithmetic (use BigDecimal, not double)
- Timezone mismatch in date comparisons
- Off-by-one in pagination offset/limit
- Race condition when two requests modify same account concurrently
Exploratory Testing: After completing the test suite, manually call the endpoint via Postman or REST Assured with unexpected inputs (empty body, extra fields, SQL injection strings, Unicode characters). Log anomalies as new test cases.
Checklist-Based Testing: For each PR touching a service layer, verify:
- Null/empty input handled, not NPE
- Repository returns Optional, unwrapped safely
- Exceptions mapped to appropriate HTTP status codes
- Logging does not include PII (names, account numbers, card numbers)
- Transaction boundaries correct (@Transactional where needed)
- Idempotency for payment endpoints
TDD Cycle (Red → Green → Refactor)
For new service methods, follow TDD:
- Red: Write a failing test that defines the expected behavior
- Green: Write the minimum code to make it pass (no gold-plating)
- Refactor: Clean up while keeping tests green
- Repeat for the next behavior
// Step 1 — RED (write this first, it fails)
@Test
void transfer_debits_source_account() {
Account source = AccountFixture.withBalance(new BigDecimal("500.00"));
transferService.transfer(source, destination, new BigDecimal("100.00"));
assertThat(source.getBalance()).isEqualByComparingTo("400.00");
}
// Step 2 — GREEN (write just enough to pass)
// Step 3 — REFACTOR (extract constants, improve naming)
Unit Test Structure (AAA Pattern — ISTQB)
Every test follows Arrange → Act → Assert. No exceptions.
@ExtendWith(MockitoExtension.class)
class TransferServiceTest {
@Mock private AccountRepository accountRepository;
@Mock private TransactionRepository transactionRepository;
@Mock private NotificationService notificationService;
@InjectMocks private TransferService transferService;
@Test
@DisplayName("should debit source account when transfer is valid")
void transfer_debits_source_account_when_valid() {
// Arrange
Account source = AccountFixture.withBalance(new BigDecimal("500.00"));
Account destination = AccountFixture.withBalance(BigDecimal.ZERO);
when(accountRepository.findById(source.getId())).thenReturn(Optional.of(source));
when(accountRepository.findById(destination.getId())).thenReturn(Optional.of(destination));
// Act
transferService.transfer(source.getId(), destination.getId(), new BigDecimal("100.00"));
// Assert
assertThat(source.getBalance()).isEqualByComparingTo("400.00");
verify(transactionRepository).save(any(Transaction.class));
}
}
Test Naming
Format: methodName_expectedOutcome_whenCondition
void transfer_debits_source_account_when_transfer_is_valid()
void transfer_throws_InsufficientFundsException_when_balance_too_low()
void transfer_throws_AccountNotFoundException_when_source_does_not_exist()
void getAccountBalance_returns_zero_when_account_is_newly_created()
Use @DisplayName for human-readable labels in test reports:
@DisplayName("should throw InsufficientFundsException when transfer amount exceeds balance")
Test Doubles Strategy (ISTQB §4.4.1 — Error Guessing)
Choose the right double for each dependency:
| Double | When to Use | Mockito |
|---|---|---|
| Mock | Verify a call happened (behavioral) | @Mock + verify() |
| Stub | Control return value (state) | when(...).thenReturn(...) |
| Spy | Partial mock of real object | @Spy |
| Fake | In-memory replacement (DB, cache) | H2, Testcontainers |
| Dummy | Placeholder, not used in test | null or mock() without setup |
Rule: Prefer fakes (H2, Testcontainers) over mocks for repositories. Mocking the DB hides real query behavior.
Slice Tests (Component Integration — ISTQB)
Test one layer with its direct dependencies. Faster than full context. Use Spring Boot test slices.
// Repository slice — only JPA layer loaded
@DataJpaTest
class AccountRepositoryTest {
@Autowired AccountRepository accountRepository;
@Test
void findByUserId_returns_all_accounts_for_user() {
// Uses H2 in-memory DB automatically
Account saved = accountRepository.save(AccountFixture.forUser("user-123"));
List<Account> result = accountRepository.findByUserId("user-123");
assertThat(result).hasSize(1).extracting(Account::getId).contains(saved.getId());
}
}
// Web/Controller slice — only MVC layer loaded
@WebMvcTest(TransferController.class)
class TransferControllerTest {
@Autowired MockMvc mockMvc;
@MockBean TransferService transferService;
@Test
void POST_transfer_returns_202_when_request_is_valid() throws Exception {
when(transferService.transfer(any(), any(), any()))
.thenReturn(TransactionFixture.pending());
mockMvc.perform(post("/api/v1/transfers")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"sourceAccountId":"acc-1","destinationAccountId":"acc-2","amount":100.00}
"""))
.andExpect(status().isAccepted())
.andExpect(jsonPath("$.status").value("PENDING"));
}
}
REST API Tests (System Integration — REST Assured)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureRestDocs
class TransferApiTest {
@LocalServerPort int port;
@BeforeEach
void setUp() {
RestAssured.port = port;
RestAssured.basePath = "/api/v1";
}
@Test
@DisplayName("POST /transfers returns 202 with pending status for valid request")
void post_transfer_returns_202_for_valid_request() {
given()
.header("Authorization", "Bearer " + TestTokens.activeUser())
.contentType(ContentType.JSON)
.body("""
{"sourceAccountId":"acc-1","destinationAccountId":"acc-2","amount":100.00}
""")
.when()
.post("/transfers")
.then()
.statusCode(202)
.body("status", equalTo("PENDING"))
.body("transactionId", notNullValue());
}
@Test
@DisplayName("POST /transfers returns 422 when amount exceeds balance")
void post_transfer_returns_422_when_insufficient_funds() {
given()
.header("Authorization", "Bearer " + TestTokens.activeUser())
.contentType(ContentType.JSON)
.body("""
{"sourceAccountId":"acc-1","destinationAccountId":"acc-2","amount":999999.00}
""")
.when()
.post("/transfers")
.then()
.statusCode(422)
.body("error", containsString("Insufficient funds"));
}
}
HTTP Status Code Test Matrix (always cover these):
| Scenario | Expected Status |
|---|---|
| Valid request | 200 / 201 / 202 |
| Validation failure | 400 Bad Request |
| Unauthenticated | 401 Unauthorized |
| Insufficient permissions | 403 Forbidden |
| Resource not found | 404 Not Found |
| Business rule violation | 422 Unprocessable Entity |
| Server error | 500 (only test error handling, not the error itself) |
Contract Testing (Pact — System Integration)
For services that consume each other's APIs, use consumer-driven contract testing:
// Consumer side (e.g., notification-service consuming transfer-service API)
@ExtendWith(PactConsumerTestExt.class)
@PactTestFor(providerName = "transfer-service")
class TransferServicePactTest {
@Pact(consumer = "notification-service")
RequestResponsePact transferCompletedPact(PactDslWithProvider builder) {
return builder
.given("a completed transfer exists")
.uponReceiving("a request for transfer status")
.path("/api/v1/transfers/txn-123")
.method("GET")
.willRespondWith()
.status(200)
.body(LambdaDsl.newJsonBody(body -> {
body.stringType("transactionId", "txn-123");
body.stringMatcher("status", "COMPLETED|PENDING|FAILED", "COMPLETED");
body.decimalType("amount", 100.00);
}).build())
.toPact();
}
}
Risk-Based Test Prioritization (ISTQB §5.1.5 + §5.2)
Classify every test area by likelihood × impact before writing tests:
| Area | Likelihood | Impact | Priority | Coverage Target |
|---|---|---|---|---|
| Payment processing | High | Critical | P0 | 100% branch + API contract |
| KYC / compliance logic | Medium | Critical | P0 | 100% branch |
| Account CRUD | High | High | P1 | 90% branch |
| Notification delivery | Medium | Medium | P2 | 80% branch |
| Admin reporting | Low | Low | P3 | Smoke test only |
Risk matrix (ISTQB §5.2.1): Risk level = likelihood × impact. Higher risk = more tests, more techniques, more independence.
Entry and Exit Criteria (ISTQB §5.1.3)
Entry criteria before testing a story (Definition of Ready):
- Acceptance criteria defined in Given/When/Then
- API contract documented (OpenAPI spec or Pact)
- Test data strategy defined (fixtures, seed scripts)
- Environment available (local Docker or CI)
Exit criteria before merging (Definition of Done):
- All unit tests pass
- JaCoCo branch coverage ≥90% for changed classes
- All API tests pass against real Spring context
- No P0/P1 defects open
- Regression suite passes
- Contract tests pass (if service boundary touched)
Incremental Workflow
When testing a new service or feature, process one class at a time:
Order: Repository → Domain Model → Service → Controller → API integration
For each class:
1. Write test
2. Run: ./gradlew test --tests "*.ClassName*"
3. PASS → mark done, continue
4. FAIL → fix before moving on
Defect Reporting (ISTQB §5.5)
When a test exposes a bug, log it with:
Title: [Service/Endpoint] [Behavior] when [Condition]
Severity: Critical / Major / Minor
Environment: Local / CI / Staging
Steps to Reproduce:
1. Call POST /api/v1/transfers with amount: -1
2. Observe response
Expected: 400 Bad Request with validation message
Actual: 500 Internal Server Error (NPE in TransferService.java:47)
Reproducing Test: TransferServiceTest.transfer_throws_validation_exception_for_negative_amount()
Root Cause Classification (ISTQB §1.2.3):
Error: Developer did not validate input before use
Defect: Missing @Positive constraint on amount field
Failure: 500 returned to client
Always link the defect to the failing test. Always classify whether it's an Error (human mistake), Defect (bug in code), or Failure (observable symptom).
Static Testing Checklist (ISTQB §3) — PR Review
Before merging any backend PR, verify:
- SpotBugs / SonarQube scan clean (no new issues)
- No raw SQL in service layer (use JPA or named queries)
- BigDecimal used for all monetary values (not double/float)
- No PII logged (account numbers, names, card data)
- @Transactional boundaries correct
- Exception messages don't expose internal stack traces to clients
- New endpoint covered by OpenAPI spec
- Input validation annotations present (@NotNull, @Positive, @Size)
ATDD: Acceptance Criteria → Tests (ISTQB §4.5.3)
User story acceptance criteria drive test design. Before writing any test, confirm the acceptance criteria is in Given/When/Then format.
Given a KYC-verified user with account balance of 500.00
When they request a transfer of 100.00 to a valid destination account
Then the source account balance is 400.00
And a PENDING transaction record is created
And the destination account receives a credit notification
This maps directly to:
- 1 unit test:
TransferService.transfer()debits source - 1 unit test:
TransactionRepository.save()called with PENDING status - 1 integration test: full flow through service + DB
- 1 API test: POST /transfers returns 202 with correct body
Performance Benchmarks (ISTQB Non-Functional)
Reference targets (adjust for your SLAs):
| Endpoint Type | p95 Response Time |
|---|---|
| Simple GET (read) | < 100ms |
| Complex query / aggregation | < 500ms |
| Write operation | < 1000ms |
| File upload / batch | < 5000ms |
For load testing use k6 or Gatling. Integrate into CI for nightly soak runs on payment endpoints.
Key Commands
# Run all tests
./gradlew test
# Run a specific test class
./gradlew test --tests "com.example.transfer.TransferServiceTest"
# Run with coverage report
./gradlew test jacocoTestReport
# View HTML coverage report
open build/reports/jacoco/test/html/index.html
# Run integration tests only (if tagged)
./gradlew test -Pintegration
# Run REST Assured API tests
./gradlew integrationTest
# Pact contract verification
./gradlew pactVerify
Testing Quadrants (ISTQB §5.1.7)
| Quadrant | Tests | Who Runs |
|---|---|---|
| Q1 — Technology, supports team | Unit tests, component integration tests | Developer, CI |
| Q2 — Business, supports team | API tests against acceptance criteria, ATDD | Developer + QA |
| Q3 — Business, critiques product | Exploratory testing, UAT | QA + PO |
| Q4 — Technology, critiques product | Performance tests, security scans, contract tests | QA + DevOps |
Q1 and Q2 run on every PR. Q3 runs on staging before release. Q4 runs nightly.