A pull request adds currency conversion to a checkout service. Line coverage is 94%, every test is green, and the first invoice in Japanese yen comes out a hundred times too large, because the tests only checked that a price came back. Each type of software testing catches a different kind of failure, and this suite had plenty of tests of one kind and none of the kind that would have caught it. With AI assistants now writing a share of both the code and the tests, choosing which tests to write has become the part a human has to own.

The 22 types of software testing at a glance

Most types of software testing fit into two families. Functional testing checks that the system does the right thing: given this input, that output. Non-functional testing checks how well it does it: how fast, under how much load, how safely and for which users. The map below sorts 22 types into those families. How a test is run (by hand, automated or continuously) and techniques such as mutation testing apply across all of them, so they sit underneath rather than in the count.

Map of 22 types of software testing in two families. Functional testing (9 types, does the system do the right thing): by scope: unit, integration, contract, end-to-end; change checks: smoke, regression; acceptance: user acceptance, alpha and beta, operational acceptance. Non-functional testing (13 types, how well, how safely, for whom): performance: load, stress, spike, soak, scalability; security: static analysis (sast), dependency scanning (sca), dynamic scanning (dast), penetration testing; experience: usability, accessibility, compatibility; reliability: resilience (chaos). Across all of them, not counted as types: tests run manually, automated or continuously, and techniques such as exploratory, black-box and white-box, property-based and mutation testing.22 TYPES OF SOFTWARE TESTING IN TWO FAMILIESFunctional testingdoes the system do the right thing? · 9 typesBy scope4 typesUnitIntegrationContractEnd-to-endChange checks2 typesSmokeRegressionAcceptance3 typesUser acceptanceAlpha and betaOperational acceptanceNon-functional testinghow well, how safely, for whom? · 13 typesPerformance5 typesLoadStressSpikeSoakScalabilitySecurity4 typesStatic analysis (SAST)Dependency scanning (SCA)Dynamic scanning (DAST)Penetration testingExperience3 typesUsabilityAccessibilityCompatibilityReliability1 typeResilience (chaos)ACROSS ALL 22 TYPES (NOT COUNTED AS TYPES)How tests runManualAutomatedContinuousTechniquesExploratoryBlack-box, white-boxProperty-basedMutation
Twenty-two types of software testing, grouped by what they check. Manual, automated and continuous testing, and the techniques in the bottom row, apply to any of them.

No project needs all 22 types of software testing with the same weight. A hotel metasearch engine answering hundreds of millions of queries a day needs load testing and soak tests from the first month. An internal document workflow with forty users gets more from access-control tests and a careful acceptance round than from spike tests. The useful question for each type is which failure it would catch in your system, and what that failure would cost. Answered for every type, that question is your testing strategy.

Manual vs automated vs continuous testing

These three software testing approaches describe how a test is run, not what it checks. A usability test is almost always manual; regression testing is almost always automated; and a load test can be a one-off manual exercise or a nightly pipeline job. The table compares manual testing, automation testing and continuous testing on the questions teams usually argue about.

QuestionManual testingAutomated testingContinuous testing
Who runs itA tester, developer or end user at a keyboardTest code written by developers and QA engineers, run by a build toolThe CI/CD pipeline, on every commit and deployment, plus checks against production
When it runsWhen a feature is ready to explore, and before a releaseWhen someone starts the suite, locally or in a scheduled jobOn every push, merge and deploy, without anyone starting it
Speed of feedbackHours to daysMilliseconds for unit tests, minutes for end-to-end suitesMinutes after a push; a red build blocks the merge
Cost to set upLow: a test plan, test data and an environmentMedium: test code, fixtures and a frameworkHigh: pipelines, disposable environments, test data and quality gates
Cost to runHigh, and paid again on every releaseLow per run; maintaining slow and flaky tests is the real costLow per run; build minutes and environments add up
Finds bestConfusing flows, usability problems, the bugs nobody wrote a test forRegressions in behaviour with a known expected resultIntegration breaks and regressions, minutes after the change that caused them
Tools in 2026Test management (TestRail, Xray), session notes, screen recordingJUnit 6, AssertJ, Mockito, Testcontainers, Playwright, GatlingGitHub Actions, GitLab CI or Jenkins; SonarQube quality gates; feature flags
Where it failsRepetitive regression checks: people get bored and skip stepsAnything without a precise expected result; suites that grow slow until people stop running themA slow or flaky pipeline: teams start re-running jobs and overriding gates, and the signal is lost

Continuous testing is automated testing wired into every stage of the development process: unit and integration tests on each push, contract checks before a deploy, smoke tests right after it, and synthetic checks against production afterwards. It depends on the pipeline staying fast. Once a build takes forty minutes, developers batch their changes and the feedback arrives too late to point at a single commit.

Automation testing has not made manual testing obsolete: it keeps its place in exploratory sessions, usability studies and acceptance by the people who will use the system. A person notices that a flow is confusing; an assertion only checks what someone thought to write down.

Functional vs non-functional testing: what each one checks

A functional requirement says what the system does: an order over 200 euros gets free shipping, a locked account cannot log in. A test passes or fails against it with no interpretation needed.

A non-functional requirement describes an aspect of software quality, such as speed or availability, and it is only testable once someone writes it as a number. “The checkout must be fast” cannot fail. “The checkout answers 95% of requests in under 300 ms at 200 requests a second” can, and a load test can check it on every release. Write non-functional requirements as numbers before the first performance or availability test, or the tests have nothing to assert.

Functional testing types, from one class to real users

From unit testing to system testing, the functional types differ mostly in how much of the system they run for real, which sets both what they can catch and how long they take.

Which parts of a system each functional test type runs for real. Unit tests: your code real, database and queues stubbed, other services stubbed, ui and browser not involved, real users not involved; feedback in milliseconds. Integration tests: your code real, database and queues real, other services stubbed, ui and browser not involved, real users not involved; feedback in seconds. Contract tests: your code real, database and queues not involved, other services a stub generated from a contract the provider verifies, ui and browser not involved, real users not involved; feedback in seconds. End-to-end tests: your code real, database and queues real, other services real, ui and browser real, real users not involved; feedback in minutes. User acceptance tests: your code real, database and queues real, other services real, ui and browser real, real users real; feedback in days.WHAT EACH FUNCTIONAL TEST RUNS FOR REALYourcodeDatabaseand queuesOtherservicesUI andbrowserRealusersFeedback inUnitmillisecondsIntegrationsecondsContractsecondsEnd-to-endminutesUser acceptancedaysrealstubbed or mockedstub from a verified contractnot involved
What each functional test type runs for real and what it replaces with stubs, with typical feedback times.

1. Unit testing

Unit testing checks one piece of logic, usually a class or a small group of classes, in milliseconds and without a database, network or file system. A unit test is the test you run hundreds of times a day, so when performing unit testing, test behaviour through a public method so the test survives a refactoring of the internals.

The Java toolkit in 2026 is JUnit 6, released on 30 September 2025, which raises the baseline to Java 17 and gives the Platform, Jupiter and Vintage modules a single version number; AssertJ for readable assertions; and Mockito for the collaborators you replace. Domain code written with records and sealed types is easy to test, because every case is a value you can construct:

sealed interface Discount {
    record None() implements Discount {}
    record Percent(int value) implements Discount {
        Percent {
            if (value < 0 || value > 100) throw new IllegalArgumentException("percent: " + value);
        }
    }
    record Fixed(BigDecimal amount) implements Discount {}
}

final class Pricing {
    static BigDecimal apply(BigDecimal total, Discount discount) {
        return switch (discount) {
            case Discount.None _ -> total;
            case Discount.Percent(int p) -> total.multiply(BigDecimal.valueOf(100 - p)).movePointLeft(2);
            case Discount.Fixed(BigDecimal amount) -> total.subtract(amount).max(BigDecimal.ZERO);
        };
    }
}
class PricingTest {

    @ParameterizedTest
    @MethodSource("discounts")
    void appliesDiscount(Discount discount, String expected) {
        assertThat(Pricing.apply(new BigDecimal("200.00"), discount))
                .isEqualByComparingTo(expected);
    }

    static Stream<Arguments> discounts() {
        return Stream.of(
                arguments(new Discount.None(), "200.00"),
                arguments(new Discount.Percent(10), "180.00"),
                arguments(new Discount.Fixed(new BigDecimal("250.00")), "0.00"));
    }

    @Test
    void rejectsPercentOutsideRange() {
        assertThatIllegalArgumentException().isThrownBy(() -> new Discount.Percent(120));
    }
}

The parameterized test reads as a table of cases, and isEqualByComparingTo compares BigDecimal values by amount, so 180.00 equals 180.0000. Two practical notes on Mockito: mock the interfaces you own (a PriceFeed port) rather than library types, and on Java 21 and later attach Mockito as a Java agent in the Surefire configuration, since the JDK now warns about agents loaded at runtime. In Spring Boot tests, use @MockitoBean from Spring Framework; @MockBean has been deprecated since Spring Boot 3.4, and the Spring Boot 4 migration guide covers what else moves in an upgrade.

2. Integration testing

Integration testing checks that your code works with the things it talks to: a real database, a message broker, a cache. These are the tests that catch a mistyped column, a missing index that turns a query into a table scan, a transaction boundary in the wrong place, or JSON that a real Kafka consumer cannot read.

Integration tests with Testcontainers and Spring Boot

The old compromise was an in-memory H2 database standing in for PostgreSQL or Oracle, which hid every difference in SQL dialect, locking and JSON support. Testcontainers starts the real database in Docker for the test run, and Spring Boot’s @ServiceConnection points the application at it with no properties to set:

@SpringBootTest
@Testcontainers
class OrderRepositoryIT {

    @Container
    @ServiceConnection
    static PostgreSQLContainer postgres = new PostgreSQLContainer("postgres:17-alpine");

    @Autowired
    OrderRepository orders;

    @Test
    void findsOnlyOpenOrdersOfOneCustomer() {
        orders.save(Order.open("c-42"));
        orders.save(Order.open("c-42").ship());
        orders.save(Order.open("c-7"));

        assertThat(orders.findOpenByCustomer("c-42"))
                .singleElement()
                .extracting(Order::customerId)
                .isEqualTo("c-42");
    }
}

The query runs against the same PostgreSQL major version as production, with your schema migrations applied at startup. Expect seconds per test class rather than milliseconds; reusing one container across test classes keeps a suite of a few hundred integration tests within a few minutes.

What changed in Testcontainers 2.0

Testcontainers 2.0 removed JUnit 4 support, prefixed every module artifact with testcontainers- (org.testcontainers:postgresql became org.testcontainers:testcontainers-postgresql) and moved container classes into per-module packages such as org.testcontainers.postgresql, where PostgreSQLContainer is no longer generic. Spring Boot 4.0 manages Testcontainers 2.0, so a Boot upgrade brings the change with it. Moving from 1.x is mostly imports and artifact coordinates, which automated refactoring with OpenRewrite handles well.

3. Contract testing

In a system of services owned by different teams, the costly failures happen at the boundaries: the pricing team renames a field, their tests pass, and checkout breaks in production. End-to-end tests across ten services would catch it, slowly and unreliably. Consumer-driven contract testing catches it in each team’s own build.

With Pact, the consumer writes a test against a mock provider that records what it sends and what it relies on in the response:

@ExtendWith(PactConsumerTestExt.class)
@PactTestFor(providerName = "pricing-service")
class PricingClientPactTest {

    @Pact(consumer = "checkout-service")
    V4Pact priceOfKnownSku(PactDslWithProvider builder) {
        return builder
                .given("SKU A-100 exists")
                .uponReceiving("a price request for A-100")
                    .path("/prices/A-100")
                    .method("GET")
                .willRespondWith()
                    .status(200)
                    .body(new PactDslJsonBody()
                            .stringType("sku", "A-100")
                            .decimalType("amount", 19.99)
                            .stringMatcher("currency", "[A-Z]{3}", "EUR"))
                .toPact(V4Pact.class);
    }

    @Test
    void readsPriceAndCurrency(MockServer mockServer) {
        var client = new PricingClient(URI.create(mockServer.getUrl()));

        assertThat(client.priceOf("A-100").currency()).isEqualTo("EUR");
    }
}

The test produces a contract file, which goes to a Pact Broker. The provider’s pipeline replays every consumer’s contract against the real pricing service and fails if a field the checkout relies on has gone. Before a deploy, the broker’s can-i-deploy check answers whether this version is compatible with what is running in the target environment. The matchers matter: stringType asserts that sku is a string, not that it equals “A-100”, so the contract pins the shape the consumer needs and nothing more. Spring Cloud Contract is the provider-driven alternative for teams that are all on Spring.

4. End-to-end (system) testing

An end-to-end test drives the deployed system the way a user would, through the browser or the public API, with every service, database and queue running. System testing is the older name for the same level, and many a test plan still uses it. It is the only test that proves a journey works with all the real configuration in place, and it is also the slowest and flakiest test you own.

Keep end-to-end suites to the journeys that make money or carry legal risk: sign-up, checkout, payment, a claim submission. Playwright for Java is a sound default for new suites: it waits for elements automatically and runs Chromium, Firefox and WebKit from one API. When an end-to-end test fails, write the narrower test that would have caught the same bug, so the next occurrence fails in seconds rather than at the end of the pipeline. For services without a user interface, API testing with REST Assured fills the same role at a lower cost.

5. Smoke testing (and sanity testing)

Smoke testing means a handful of quick checks run right after a deploy: the application starts, the health endpoint reports its database and broker as up, a user can log in, the main page renders. If smoke tests fail, the pipeline rolls back before anyone runs the longer suites. A Spring Boot Actuator health check plus three or four API calls is enough for most services.

Sanity testing is the narrower check after a small fix: confirm the bug is gone and the existing functionality around it still works, before spending time on full regression testing.

6. Regression testing

Regression testing reruns the existing suite to confirm that a change did not break existing functionality. In a continuous pipeline it is less a separate activity than a discipline: every bug fix starts with a test that reproduces the bug, and that test stays in the suite.

Regression tests matter most before a large change. When we moved a London wine merchant’s platform from Java 17 and Spring 5 to Java 25, we first added regression tests around PDF invoices, spreadsheet imports, product search, orders and billing, plus browser tests through both applications. During the upgrade they caught two breaks before release: one would have stopped all PDF generation, the other would have broken spreadsheet imports. For a legacy system modernization, that safety net comes before the first framework moves.

Acceptance testing: UAT, alpha and beta, operational

Acceptance testing answers a different question from the functional testing above: does the system meet user expectations and the needs of the people who operate it, rather than what the specification says?

7. User acceptance testing (UAT)

In user acceptance testing, the people who will use the system run real scenarios on production-like data and sign off. UAT finds the gap between the requirement as written and the job as done, such as a claims handler who needs three fields on one screen. Book the testers’ time in advance; acceptance testing competes with everyone’s day job and slips when nobody has hours set aside for it.

8. Alpha and beta testing

Alpha testing puts the product in front of internal users; beta testing widens it to a group of real customers. In 2026 most web products do this with feature flags and staged rollouts rather than separate beta builds: the new flow goes to staff, then to 5% of users, with error rates and support tickets watched at each step.

9. Operational acceptance testing

Operational acceptance testing (OAT) checks that the system can be run, not just used: backups restore, failover works, alerts fire and reach someone, logs carry a correlation ID, and the runbook matches the system. It is the test most often skipped and the one an incident at 3 a.m. depends on.

Test pyramid or testing trophy: how many of each test to write

Once the types are clear, the argument moves to proportions, and to the testing strategy behind them. The test pyramid, which Mike Cohn introduced and Martin Fowler’s site popularized, says to write many fast unit tests, fewer integration tests and a few end-to-end tests. The testing trophy from Kent C. Dodds moves the weight to integration tests, with static analysis as the base.

Two shapes for a test suite. The test pyramid, after Mike Cohn: a wide base of unit tests, a middle band of integration tests and a narrow top of end-to-end tests. The testing trophy, after Kent C. Dodds: from top to bottom a small end-to-end layer, the largest layer of integration tests, a smaller layer of unit tests and a base of static analysis.TEST PYRAMIDTESTING TROPHYEnd-to-endIntegrationUnitEnd-to-endIntegrationUnitStatic analysisMike Cohn: many fast unit tests, fewerintegration tests, a few end-to-end testson top.Kent C. Dodds: most effort on integrationtests; static analysis and types catch thecheap mistakes.
The test pyramid and the testing trophy. The width of each layer stands for the share of testing effort, not an exact count.

Much of the disagreement is about words. Fowler’s essay on test shapes points out that people mean different things by “unit test”: a solitary test with every collaborator mocked, or a sociable test that runs a few real classes together. A sociable unit test in one team’s pyramid is an integration test in another team’s trophy.

Our position for Java services: let the cost of feedback decide. Testcontainers made a real-database test cost seconds, so for a Spring Boot service whose logic is mostly persistence and calls to other services, the trophy fits: most tests should run against real infrastructure, because that is where the bugs are. For code where the logic is the product, such as pricing rules, ledger postings or schedule calculations, the pyramid still wins: thousands of millisecond unit and property tests cover combinations no integration suite could reach. Most systems contain both kinds of code, so most need both shapes in different modules.

The shape to avoid is the ice-cream cone: few unit tests, some integration tests and a large, slow, manual or end-to-end layer on top. It usually grows when tests are added only after bugs reach production.

Non-functional testing types: performance, security, experience and reliability

Non-functional tests rarely belong on every pull request; a soak test takes hours and a penetration test takes days. The matrix shows a cadence that works for most Java services, and the tools we would reach for first.

When to run each non-functional test on a typical Java service, and with which tools. Load: lighter run every pr, full run nightly, full run pre-release; tools: Gatling, k6, JMeter. Stress: full run pre-release; tools: Gatling, k6, JMeter. Spike: full run pre-release; tools: Gatling, k6. Soak: full run pre-release; tools: Gatling, k6, JFR. Scalability: lighter run pre-release, full run scheduled; tools: Gatling, k6. Static analysis: full run every pr; tools: SonarQube, SpotBugs. Dependency scan: full run every pr, full run nightly; tools: Dependency-Check, OSV. Dynamic scan: lighter run every pr, full run nightly; tools: ZAP. Penetration test: lighter run pre-release, full run scheduled; tools: Specialist testers. Usability: full run pre-release, lighter run scheduled; tools: Sessions with users. Accessibility: lighter run every pr, full run pre-release; tools: axe-core, manual audit. Compatibility: lighter run every pr, full run nightly; tools: Playwright, JDK matrix. Resilience (chaos): lighter run pre-release, full run scheduled; tools: Toxiproxy, Chaos Mesh.Every PRNightlyPre-releaseScheduledTypical toolsPERFORMANCELoadGatling, k6, JMeterStressGatling, k6, JMeterSpikeGatling, k6SoakGatling, k6, JFRScalabilityGatling, k6SECURITYStatic analysisSonarQube, SpotBugsDependency scanDependency-Check, OSVDynamic scanZAPPenetration testSpecialist testersEXPERIENCEUsabilitySessions with usersAccessibilityaxe-core, manual auditCompatibilityPlaywright, JDK matrixRELIABILITYResilience (chaos)Toxiproxy, Chaos Meshfull runlighter run: a short baseline, one browser, an automated subset
When to run each non-functional test on a typical Java service. Shift the cadence toward the left for systems where that quality is the product.

Performance testing

Performance testing measures response time, throughput and resource use under a defined load. The five types below differ mostly in the shape of that load over time.

Load shapes of five performance tests. Load testing ramps users up to the expected peak, holds and ramps down: does the system meet its targets at peak? Stress testing raises load in steps past the peak until the system breaks: where does it break and does it recover? Spike testing jumps from a low baseline to nearly double the peak and back: does the system survive a surge? Soak testing holds load near the peak for many hours: does the system degrade? Scalability testing measures throughput as nodes are added against a line of linear scaling.FIVE PERFORMANCE TESTS, FIVE LOAD SHAPESLoadusers ↑time →Does the systemhold its targetsat peak load?Stressusers ↑time →Where does thesystem break, anddoes it recover?Spikeusers ↑time →Does the systemsurvive a suddensurge?Soakusers ↑hours →Does the systemdegrade over manyhours?Scalabilitythroughput ↑nodes →Does throughputgrow as nodes areadded?expected peak (for scalability: linear scaling)
Load shapes of the five performance test types, drawn schematically.

Report percentiles, not averages: a 50 ms average can hide a p99 of four seconds, and the p99 is what your heaviest users experience. Use an open workload model (new users arrive at a set rate whether or not earlier requests have finished) when real traffic behaves that way; a closed model with a fixed number of virtual users slows its own arrival rate when the system slows down, which flatters the results. Gatling’s Java DSL keeps the load test in the same language and repository as the service, and its assertions turn the run into a pass or a fail for the pipeline:

import static io.gatling.javaapi.core.CoreDsl.*;
import static io.gatling.javaapi.http.HttpDsl.*;

public class CheckoutSimulation extends Simulation {

    HttpProtocolBuilder protocol = http.baseUrl("https://staging.example.com");

    ScenarioBuilder checkout = scenario("Checkout")
            .exec(http("view cart").get("/api/cart"))
            .pause(1)
            .exec(http("pay").post("/api/checkout")
                    .body(StringBody("{\"cartId\": \"c-1\"}")).asJson());

    {
        setUp(checkout.injectOpen(rampUsersPerSec(10).to(200).during(Duration.ofMinutes(10))))
                .protocols(protocol)
                .assertions(
                        global().responseTime().percentile(95.0).lt(300),
                        global().failedRequests().percent().lt(1.0));
    }
}

k6 is the main alternative, with scripts in JavaScript or TypeScript and close integration with Grafana; Apache JMeter remains common in organizations with existing test plans. Whatever the performance testing tool, the result is only as good as the workload model. On the UK hotel metasearch engine we redesigned, the production figures after the redesign were over 300 million queries a day, 95% of them answered in under a second. A load test that cannot reproduce that query mix says little about either number. When a test fails a target, a profiler tells you why; see Java profilers and common Java performance problems.

10. Load testing

Load testing runs the expected peak, for example the Monday-morning or Black Friday traffic, and checks latency and error rates against the targets. It is the one performance test worth running on every release candidate.

11. Stress testing

Stress testing raises load in steps past the peak until something gives, to learn which component fails first (often a connection pool or a downstream service rather than your own code) and whether the system recovers once the load drops. A system that sheds load with fast errors is in better shape than one that queues requests until every caller times out.

12. Spike testing

Spike testing jumps from normal traffic to several times the peak in seconds: a marketing email, a flash sale, a retry storm after an outage elsewhere. It tests autoscaling delays, cold caches and JIT warm-up, which a slow ramp never reaches.

13. Soak (endurance) testing

Soak testing, also called endurance testing, holds a realistic load for hours, sometimes days. It finds what only time reveals: a memory leak, a growing cache with no eviction, connections that leak from the pool one at a time, log files that fill a disk. Record a Java Flight Recorder session for the whole run so the heap and GC trend is there when you need it.

14. Scalability testing

Scalability testing adds nodes or resources and measures whether throughput grows with them. Perfectly linear scaling is rare; the useful output is the curve and the point where it flattens, which shows where the shared bottleneck is (often the database) and what the next tenfold growth in traffic will cost. It is also where a performance engineering engagement should start, because it shows which component to fix first.

Security testing

Security testing finds weaknesses before an attacker does. Four types cover most of the ground, from cheap and automatic to expensive and human.

15. Static analysis (SAST)

Static application security testing reads the source code without running it, looking for injection, unsafe deserialization, hard-coded secrets and the like. SonarQube (Server or Cloud), SpotBugs with the Find Security Bugs plugin and GitHub CodeQL all handle Java. Run it on every pull request and fail the build on new critical findings only, or the backlog of old warnings teaches everyone to ignore the tool. The secure Java coding guide covers the code patterns these tools flag.

16. Dependency scanning (SCA)

Software composition analysis checks every library in the dependency tree against vulnerability databases. Most of the code in a Spring Boot application is someone else’s, and new advisories appear without any change on your side, so scan nightly as well as on each build. OWASP Dependency-Check, GitHub Dependabot and the OSV scanner all read Maven and Gradle builds. On the wine merchant’s platform, a dependency scan found 12 critical and 35 high-severity advisories; after the upgrade the same OSV scan found none. Advisories that need a framework upgrade to fix are what our Java vulnerability remediation work handles.

17. Dynamic scanning (DAST)

Dynamic application security testing attacks the running application from the outside, the way a scanner on the internet would: missing security headers, injectable parameters, exposed admin endpoints. ZAP, the open-source scanner now published as ZAP by Checkmarx, has a baseline scan that fits in a pipeline stage against a staging environment, and a full scan for nightly runs.

18. Penetration testing

A penetration test is a time-boxed attack by specialists who chain small weaknesses into a real breach, which no scanner does. Book one before a major release, after an architecture change and at least yearly for systems that hold payment or personal data. Financial entities in the EU face a stricter version: the Digital Operational Resilience Act, applied since January 2025, requires threat-led penetration testing for the firms it designates.

Usability, accessibility and compatibility testing

19. Usability testing

Usability testing watches real users try to complete real tasks, and measures where they hesitate, misread or give up. Short rounds of moderated sessions, repeated as the design changes, show where people get stuck. It is manual by nature and works best early, on a prototype, while changing a screen still takes an afternoon.

20. Accessibility testing

Accessibility testing checks that people using screen readers, keyboards or magnification can use the product, usually against WCAG 2.2 level AA. Since June 2025 the European Accessibility Act has made this a legal requirement for many consumer-facing digital services sold in the EU. Automated checks with axe-core, which has a Playwright integration for Java, catch missing labels and contrast failures on every build; a manual audit with a screen reader catches the rest before a release.

21. Compatibility testing

On the front end, compatibility testing means running the main journeys on the browsers, devices and operating systems your users have, which Playwright’s Chromium, Firefox and WebKit engines cover for most web applications. On the back end, it means a build matrix: if a library supports Java 21 and 25, CI should run its tests on both, and an application should be tested on the JDK and operating systems it will run on in production. The Java end-of-life calendar tells you which versions are worth keeping in the matrix.

22. Resilience (chaos) testing

Resilience testing injects failures on purpose: a dependency that answers in five seconds instead of fifty milliseconds, a dropped connection, a node that disappears. It checks the timeouts, retries, circuit breakers and fallbacks that only run when something else goes wrong, which is exactly when nobody has tested them. Toxiproxy, which has a Testcontainers module, adds latency and cuts connections inside an ordinary integration test; Chaos Mesh does the same at cluster level on Kubernetes. Start small, with one dependency made slow in a test environment, before any experiment in production.

Techniques that work across test types: exploratory, property-based and mutation testing

These are testing methods rather than types in their own right: ways of designing or checking tests that apply at any level.

Exploratory, black-box and white-box testing

Exploratory testing is unscripted: a tester learns the system, designs tests and runs them in the same session, following whatever looks wrong. Black-box testing works from inputs and outputs only; white-box testing uses knowledge of the code, for example to reach every branch. Most good test suites mix both, and random “monkey” testing survives today in its automated form, fuzzing.

Property-based testing

An example-based test checks one input. A property-based test states a rule that must hold for every input, and the framework generates hundreds of inputs to try to break it, then shrinks any failure to the smallest case. On the JVM, jqwik runs on the JUnit Platform and tries 1,000 inputs per property by default:

class PricingProperties {

    @Property
    void discountedTotalStaysBetweenZeroAndTotal(
            @ForAll @BigRange(min = "0", max = "1000000") @Scale(2) BigDecimal total,
            @ForAll @IntRange(min = 0, max = 100) int percent) {

        var result = Pricing.apply(total, new Discount.Percent(percent));

        assertThat(result).isBetween(BigDecimal.ZERO, total);
    }
}

Properties work well for parsers, serializers (decode after encode returns the input), money and date arithmetic, and any code with an invariant you can state in one sentence. They also counter the pesticide paradox, where a suite that always feeds the same inputs stops finding new bugs. One thing to know before adopting it: since version 1.10, jqwik ships with an anti-AI usage clause and prints a line addressed to AI coding agents on every test run, which matters if agents run your test suite.

Mutation testing

Coverage tells you which lines a test executed, not whether it checked anything. Mutation testing measures that directly: PIT makes small changes to the compiled code, such as turning a - into a + or a < into a <=, and runs your tests against each mutant. A mutant that no test notices has survived, and points at a line that is covered but not tested. This test covers the percent branch fully and kills almost nothing:

@Test
void appliesPercentDiscount() {
    var result = Pricing.apply(new BigDecimal("200.00"), new Discount.Percent(10));
    assertThat(result).isNotNull();   // line covered, behaviour unchecked
}

PIT changes 100 - p to 100 + p, the method returns 420.00 instead of 180.00, and the test still passes. The parameterized test earlier kills that mutant, and so does the jqwik property. PIT runs JUnit Platform suites through its pitest-junit5-plugin, placed on the plugin’s classpath rather than the project’s; its README lists the platform versions each release was built against, so check it before moving to JUnit 6.

<plugin>
    <groupId>org.pitest</groupId>
    <artifactId>pitest-maven</artifactId>
    <version>${pitest.version}</version>
    <dependencies>
        <dependency>
            <groupId>org.pitest</groupId>
            <artifactId>pitest-junit5-plugin</artifactId>
            <version>${pitest-junit5.version}</version>
        </dependency>
    </dependencies>
    <configuration>
        <targetClasses>
            <param>com.example.pricing.*</param>
        </targetClasses>
        <mutationThreshold>80</mutationThreshold>
    </configuration>
</plugin>

Run mvn test-compile org.pitest:pitest-maven:mutationCoverage for a full report, or the scmMutationCoverage goal to mutate only the classes changed in the working copy, which keeps the run short enough for a pull request. The mutationThreshold fails the build when the share of killed mutants drops below 80%.

Six rules for testing AI-generated and vibe-coded software

AI coding assistants are now ordinary tools: in the Stack Overflow Developer Survey 2025, 84% of respondents use or plan to use them and 51% of professional developers use them daily. The same survey found that 46% distrust the accuracy of AI output, against 33% who trust it. Google’s DORA research describes AI as an amplifier that magnifies a team’s existing strengths and weaknesses (State of AI-assisted Software Development, 2025). For software testing, that means a weak suite lets the defects in generated code through at the same higher speed as the code itself.

Vibe coding, the term Andrej Karpathy coined in early 2025 for building software by prompting and accepting the output without reading it closely, takes this to its limit: the tests become the only part of the system anyone has reasoned about. The six rules below build on the classic principles of good testing (start early, avoid the pesticide paradox, automate, test in production-like conditions, keep testing continuous) and apply them to generated code. For the assistants themselves, see our comparison of the best AI coding tools.

Six rules for testing AI-generated code, placed where they act in the delivery pipeline. Before code: rule 1, tests first: a human writes or approves the tests before generated code is accepted. Review: rule 2, two authors: one model never writes both the code and its only tests. Every build: rule 3, tests that bite: mutation and property-based tests on the code that changed. rule 4, real boundaries: contract and integration tests against real databases and APIs. rule 5, scan everything: sAST on the code, SCA on dependencies, a check that each new package is real. Merge: rule 6, merge gates: coverage and mutation thresholds on new code; no new critical findings.Before codeRULE 1Tests firstA human writesor approves thetests beforegenerated codeis accepted.ReviewRULE 2Two authorsOne model neverwrites both thecode and itsonly tests.Every buildRULE 3Tests that biteMutation andproperty-based tests onthe code that changed.RULE 4Real boundariesContract and integrationtests against realdatabases and APIs.RULE 5Scan everythingSAST on the code, SCA ondependencies, a checkthat each new package isreal.MergeRULE 6Merge gatesCoverage andmutationthresholds onnew code; nonew criticalfindings.
Six rules for testing AI-generated code, placed at the stage of the pipeline where each one applies.

1. Write or approve the tests before you accept generated code

In an AI-assisted development process, treat the tests as the specification. Before an assistant writes the implementation, a developer writes the key test cases, or reviews and corrects the ones the assistant proposes, including the edge cases: empty input, the currency with no minor unit, the leap day, the customer with two open orders. If the tests were generated alongside the code, passing them shows only that the code agrees with the model’s own reading of the task.

2. Never let one model write both the code and its only tests

When the same model writes a function and its tests in one pass, both carry the same misunderstanding of the requirement, and the tests confirm it. Keep authorship of code and tests separate: a human writes one side, or at minimum reviews the tests as carefully as the code. Watch for tests that mock the very class under test, assert on values copied from the implementation, or check only that no exception was thrown.

3. Prove the tests can fail with mutation and property-based testing

Generated tests are often long and green and assert very little, like the isNotNull() example above. Run PIT on the classes a pull request changes and look at the surviving mutants before accepting the tests. Add property-based tests for anything with an invariant; they explore inputs that neither the model nor the reviewer thought of, which is where generated code tends to be wrong.

4. Test the boundaries against real systems

Assistants write plausible code against APIs and schemas they have only partly seen: a column name from an older migration, a response field the provider never sent, a library method from a different major version. Unit tests with mocks reproduce the same guess. Integration testing against a real database in Testcontainers, together with contract tests verified by the provider, is where those guesses fail. Run them with data shaped like production (volumes, encodings, nulls, the long tail of odd records), since a test database of ten tidy rows tells you little.

5. Scan generated code and every dependency it adds

Run SAST on generated code like any other code, and review every new dependency an assistant suggests. Models invent package names, and attackers register them: the practice is called slopsquatting. In a study of 576,000 code samples generated by 16 models, Spracklen and colleagues found that at least 5.2% of packages suggested by commercial models and 21.7% by open-source models did not exist, with 205,474 unique invented names (USENIX Security 2025 paper). The study covered Python and JavaScript. Maven Central’s verified group IDs make it harder to publish under an established company’s namespace, but not under a plausible new one, so check that each new artifact exists, is the one you meant, and is maintained before it reaches the build.

6. Gate merges on new code in CI

Put the rules into the pipeline rather than relying on reviewers’ memory. A quality gate on new code, such as SonarQube’s, can require a coverage level on changed lines, a mutation score from PIT, no new critical security findings and no new dependency with a known vulnerability. Gating on new code instead of the whole codebase keeps the gate passable on a legacy system while ensuring that nothing new arrives without tests. Keep the pipeline fast enough that nobody is tempted to bypass it; continuous testing only works while people trust the signal.

Checking a Java test suite you inherited

The rules are easiest to apply on a new codebase. On an existing one, five checks show within a day how far the current suite can be trusted:

  1. Run PIT on the module that holds the core business logic. A mutation score far below the line coverage means the tests execute code without checking it.
  2. List what the integration tests replace with mocks or in-memory substitutes. Every mocked database, queue or external API is a boundary that no test has exercised for real.
  3. Time the suite and count the tests that were retried or disabled in the last month. Flaky tests that everyone re-runs are tests nobody believes.
  4. Find the non-functional targets. If response time, throughput and availability are not written down as numbers, no performance test can pass or fail.
  5. Run a dependency scan and count the critical and high advisories, and how many need a framework upgrade to fix.

The answers usually point to a few weeks of work: a safety net of regression tests around what the business depends on, real-infrastructure integration tests in place of mocks, and gates in CI. We have kept business-critical Java systems running and changing since 2013, and we build that safety net before anything else moves. If your suite is green while production keeps finding bugs, tell us about the system and we will tell you where we would start.