Types of Software Testing and the Role of AI

testes de software Types of Software Testing

A test suite can be green and a pull request can still introduce a serious problem. That happens because no single type of test can cover every risk created by a change.

  • Unit tests validate isolated rules.
  • Integration tests check whether different parts of the system still communicate correctly.
  • Functional tests protect real product flows.
  • Security and performance tests help uncover issues that are unlikely to appear during local development.

That is why I would not look only at the test suite result. Before asking whether the tests passed, I would try to understand what kind of risk the change introduced and which type of validation is most likely to catch it.

This became even more important as AI entered the software development workflow. Copilots and agents speed up the writing of code and tests, while AI code review tools analyze diffs before merge. The volume of changes grows, but the time available to understand each one does not always grow with it.

In this article, I will walk through the main types of software testing, explain where each one helps, show what automation cannot validate on its own, and discuss how tests, CI, code review, and AI can work together before merge.

What is software testing?

Software testing checks whether a system still behaves as expected after a change. It helps teams find defects, regressions, and other risks before code reaches production.

There are different types of testing because each one looks at a different part of the system. Some validate isolated rules, while others check integrations, complete workflows, security, or performance.

In practice, choosing the right test is a risk decision. The team needs to understand what could go wrong, what the impact would be, and which type of validation is most likely to catch the problem.

How to choose the right type of test

Understanding the different types of software testing is important, but classification alone does not solve the problem. In day-to-day work, a team does not just need to know the difference between unit, integration, and end-to-end tests. It needs to decide which combination of validations makes sense for the change being made.

I would start by looking at the risk introduced by the pull request.

If the change is limited to an isolated rule, unit tests may be enough. When it changes how components, services, or dependencies communicate, integration tests become more relevant. Changes to critical product flows may require broader functional validation. Changes involving authentication, permissions, sensitive data, or high-impact areas should rarely depend on a single testing layer.

The size of the change does not determine the required effort on its own. A small diff can modify a critical rule, while a larger PR may only reorganize internal code that is already protected by a reliable regression suite. That is why I would avoid choosing the type of test based only on the amount of code changed.

The question that should guide this decision is: what could go wrong with this change, what would the impact be, and which type of validation is most likely to catch the problem before production?

From there, the team can distribute effort more effectively across automated tests, CI, code review, and targeted validations instead of trying to cover every risk in the same way.

Main types of software testing

The main types of software testing cover different risks and help teams validate specific parts of a system.

A team does not need to use all of them with the same intensity. The right combination depends on the architecture, the product, the frequency of changes, and, most importantly, the impact a failure would have in production.

Unit tests

Unit tests validate small parts of the code in isolation, such as functions, methods, classes, or components.

A simple example would be testing a function that calculates a discount:

function calculateDiscount(price, percentage) {
  return price - price * (percentage / 100);
}

test('applies a 10% discount', () => {
  expect(calculateDiscount(100, 10)).toBe(90);
});

This test validates a specific rule without depending on a database, API, queue, or any other part of the system. That makes it fast to run and easy to interpret when it fails.

Unit tests stop being sufficient when behavior depends on interactions with other parts of the system. Those dependencies can be simulated with mocks, but when almost everything is replaced, the test starts validating the simulation more than the actual application behavior.

In that scenario, the test may continue to pass even when the integration between components is broken. It shows that the unit works under simulated conditions, but it does not guarantee that the complete system still works correctly.

Integration tests

Integration tests check whether different parts of the system continue to work correctly together.

They help uncover problems that are unlikely to appear in isolated tests, such as incompatible contracts, incorrect schemas, migrations that affect queries, serialization errors, missing configuration, or failures when communicating with external services.

These tests usually require more infrastructure and take longer to run than unit tests, but they are important whenever behavior depends on interactions between modules, databases, queues, or services.

A simple example would be a change to the response format of an internal API:

// Payment service
return {
  paymentId: payment.id,
  status: payment.status,
};

If another service still expects the id property, the unit tests on both sides may continue to pass if they use outdated mocks. The integration, however, is already broken.

An integration test that calls the real service and validates the response format can catch this incompatibility before it reaches production.

Functional tests

Functional tests check whether the software delivers the expected behavior based on a business rule or product requirement.

The focus is on the outcome a user or another system can observe, not on how the code was implemented. Instead of testing an isolated function, this type of test validates whether an important flow still works, such as creating an account, approving a payment, or canceling a subscription.

They can be executed manually or automated. When they interact with the interface and involve several system components, they may take the form of end-to-end tests.

import { test, expect } from "@playwright/test";

test("allows an approved purchase to be completed", async ({ page }) => {
  await page.goto("/checkout");

  await page.fill('[name="cardNumber"]', "4242424242424242");
  await page.click('button[type="submit"]');

  await expect(page.getByText("Payment approved")).toBeVisible();
});

In this example, the test does not check how each function was implemented. It validates the behavior that matters to the person using the product: after submitting a valid payment, the purchase should be approved.

I would avoid moving every behavior to this level. The more parts involved in the execution, the higher the cost tends to be to run the test, keep it stable, and identify the cause of a failure.

That is why I would reserve this type of test for critical product flows. Smaller rules or more isolated behaviors are usually protected more efficiently by unit or integration tests.

Regression tests

Regression tests check whether a new change broke something that was already working.

They are not a specific testing technique. A regression suite can include unit, integration, functional, and end-to-end tests. What connects them is the goal of protecting existing behavior and preventing known problems from returning.

A simple example would be a bug that allowed expired coupons to remain valid during checkout. After fixing the issue, the team can add a specific test to ensure that behavior does not return:

import { describe, expect, it } from "vitest";

describe("applyCoupon", () => {
  it("does not apply expired coupons", () => {
    const coupon = {
      discount: 20,
      expiresAt: new Date("2026-07-01"),
    };

    const total = applyCoupon(100, coupon, new Date("2026-07-29"));

    expect(total).toBe(100);
  });
});

This test becomes part of the regression suite because it protects behavior that has already failed in the past.

This is one of the areas where automation usually delivers the most value. When the same flow needs to be validated after every change, relying on a person to repeat the work increases delivery time and the chance of inconsistency.

At the same time, a large, slow, and unstable suite can lose its usefulness. When false failures become common, the team starts rerunning jobs until they pass or stops trusting the CI pipeline altogether.

That is why I prefer a smaller, reliable suite connected to real product risks over a huge number of tests that no one can interpret when they fail.

Performance tests

Performance tests evaluate how a system behaves under load, volume, concurrency, or prolonged use.

They help the team understand whether an endpoint can handle the expected traffic, whether a query remains efficient as the database grows, or whether a queue can process messages at the same rate they arrive.

This type of problem often goes unnoticed during development because many bottlenecks do not appear in a local environment. A query without an index, repeated database calls inside a loop, or heavy processing executed synchronously may look acceptable with a small dataset and cause serious latency in production.

Performance tests try to reproduce conditions closer to real usage in order to measure response time, resource consumption, capacity, and stability.

A simple example using k6 would be simulating several users accessing the same endpoint at the same time:

import http from "k6/http";
import { check, sleep } from "k6";

export const options = {
  vus: 50,
  duration: "30s",
};

export default function () {
  const response = http.get("https://api.example.com/orders");

  check(response, {
    "returns status 200": (res) => res.status === 200,
    "responds in under 500 ms": (res) =>
      res.timings.duration < 500,
  });

  sleep(1);
}

This test keeps 50 virtual users active for 30 seconds and checks whether the endpoint continues to respond correctly and within the expected time.

Code review acts as a complementary layer. It can identify patterns that deserve attention before the change even reaches the tests, such as repeated queries, improper cache usage, or heavy processing in critical paths.

Security tests

Security tests help identify vulnerabilities, misconfigurations, and exploitable paths before a change reaches production.

This validation can involve static analysis, dynamic analysis, dependency scanning, penetration testing, fuzzing, and manual review of sensitive areas of the system. The right approach depends on the type of risk involved and the area changed by the pull request.

Many security problems do not appear as obvious failures. A missing authorization check, validation performed only on the client side, sensitive data written to logs, or overly broad permissions can pass functional tests and still create serious risk.

A simple example would be validating that a user can access only resources from their own organization:

import { describe, expect, it } from "vitest";

describe("GET /accounts/:id", () => {
  it("blocks access to accounts from another organization", async () => {
    const response = await request(app)
      .get("/accounts/account-456")
      .set("Authorization", "Bearer token-for-user-without-access");

    expect(response.status).toBe(403);
  });
});

This test protects a specific authorization rule. It does not replace other security validations, but it helps ensure that a change to the endpoint does not expose data from another organization.

Specialized tools are still essential. AI-assisted code review can work as a complementary layer here, especially when a diff changes authentication, authorization, encryption, personal data, or communication between services.

Manual and exploratory testing

Manual testing remains important when the team needs to evaluate usability, investigate unexpected behavior, or validate new parts of the product.

A person exploring the system can notice problems that automation was never designed to find. This is especially useful when the expected behavior is still being defined or when the evaluation depends on context and judgment.

The problem starts when manual testing becomes the default way to validate repetitive and predictable tasks. In that scenario, the process becomes slower, less consistent, and more dependent on the attention of the person running it.

Critical flows, known regressions, and stable rules should be automated whenever the cost makes sense. Manual work creates more value when it is focused on investigation and discovery rather than repeating the same checklist before every release.

Which test should you use for each change?

A practical way to choose the right test is to connect each change to the risk it introduces.

  • Changes to small, isolated rules usually call for unit tests.
  • Changes involving databases, queues, APIs, and contracts between modules call for integration tests.
  • Changes to critical user flows may require functional or end-to-end tests.
  • Fixed bugs should become regression cases whenever possible.
  • Changes to authentication, permissions, or sensitive data need security validation.
  • Changes to queries, batch processing, or high-volume paths may require performance tests.

This analysis helps avoid two extremes.

The first is relying almost entirely on unit tests and discovering too late that the components do not work together. The second is testing everything at the highest level and creating a suite that is slow, expensive, and difficult to maintain.

In most systems, the best design combines many fast tests with a smaller number of more expensive tests for the flows where a failure would have the greatest impact.

What automated tests cannot validate on their own

Even a good automated test suite leaves blind spots.

Tests validate scenarios and expectations that someone decided to write. They do not understand, on their own, the intent behind a pull request, the team’s incident history, or the architectural rules that guide the project.

That means some problems can still get through even when the entire suite is green:

  • a change that violates an important team convention;
  • a function with no coverage for its riskiest edge case;
  • an authorization check added to one endpoint but forgotten in another;
  • an improper dependency between layers;
  • a test that protects incorrect behavior;
  • an implementation that works but makes the system harder to maintain;
  • a pull request that is too large to review carefully.

This is where code review remains necessary.

Automated tests tell you whether a declared expectation was met. Code review helps determine whether that expectation is correct, whether the change makes sense in the context of the system, and whether any relevant risk was left without validation.

How AI can help with software testing

AI can help when it reduces repetitive work and directs the team’s attention toward the risks that actually matter.

The problem appears when it generates generic tests, obvious comments, or suggestions that do not consider the context of the project. In that case, instead of improving the review process, it simply adds more noise.

For me, the difference comes down to the context available. An AI system looking only at an isolated function can suggest basic cases. A tool that understands the diff, the repository, the team’s rules, and related files has a better chance of identifying a meaningful gap before merge.

Suggesting tests during the pull request

Inside a pull request, AI can analyze the changed code and identify scenarios that still need validation.

If a function gains a new condition, it may notice that one branch remains uncovered. If an endpoint starts accepting a new field, it can raise validation cases that have not yet been considered. When a migration changes a column used by other modules, it can point out which integrations deserve closer review.

The value is in helping the team notice gaps while the change is still under review. The decision about which tests actually make sense still belongs to the people who understand the expected behavior and the impact of the change.

Generating test cases

AI-powered tools can also suggest test cases based on the code, API contracts, and existing patterns in the repository.

This can speed up the writing process significantly, especially when the expected behavior is already clear and the remaining work is turning those scenarios into code.

Even so, I would review AI-generated tests with the same care used to review production code.

A test can look correct and still protect nothing meaningful. It can also become too tightly coupled to the implementation, repeat the logic being tested, or ignore the exact behavior it was supposed to guarantee.

Generating a test is easy. Making sure it fails for the right reason and protects the expected behavior is something else entirely.

Identifying edge cases

AI can also help surface questions that are easy to overlook during a quick review.

A change involving financial calculations, permissions, or error handling may require cases such as:

  • null or negative values;
  • maximum and minimum limits;
  • a user without access;
  • failure of an external API;
  • incomplete data;
  • concurrent calls;
  • repeating the same operation;
  • rollback after a partial failure.

These questions are not new. The value comes from surfacing them while the change is still under review, without relying entirely on one person to remember them.

Enforcing team rules

Some important rules do not fit neatly into a traditional test suite.

For example:

  • the domain layer must not import infrastructure;
  • every new endpoint must validate authorization;
  • changes to billing must include regression tests;
  • sensitive data must never appear in logs;
  • certain modules must not access the database directly;
  • production code must not contain debug calls.

AI code review tools can check these rules directly in the diff.

With Kodus, for example, teams can define review rules in natural language and apply them to pull requests. The goal is not to replace CI or human review, but to make recurring checks more consistent before merge.

How to combine tests, CI, and code review

Tests, CI, and code review do not need to do the same job. Each stage helps find a different type of problem, and the workflow works better when that division is clear.

Each part of the process has a different responsibility:

  • unit tests validate small rules quickly;
  • integration tests check communication between components;
  • functional tests protect important product flows;
  • regression tests prevent known problems from returning;
  • security and performance tests cover specific risks;
  • CI runs reproducible checks before merge;
  • code review evaluates intent, impact, architecture, and alignment with team rules;
  • AI helps identify gaps and apply criteria consistently.

The goal is to prevent a pull request from reaching review full of problems that could have been found automatically.

CI should block objective checks. Tests should protect important behaviors. Human review should focus on decisions that require context. AI can help connect those stages, especially when it understands the project’s rules and architecture.

Software testing starts with the risk introduced by the change

Understanding the types of software testing is important, but memorizing categories is not very useful if the team cannot connect them to product risks.

Before deciding what to test, I would try to answer:

What could break because of this change, what would the impact be, and which layer is most likely to catch the problem before production?

Some risks will be covered by automated tests. Others will require security analysis, performance validation, or a careful review of the diff.

AI can help in this process by suggesting scenarios, identifying gaps, and checking team rules inside the pull request. But it does not replace a testing strategy or the judgment of people who understand the system.

Kodus brings this analysis into the code review workflow. It considers repository context, allows teams to create custom rules, and works with GitHub, GitLab, Bitbucket, and Azure DevOps.

For teams that want to adopt AI code review with more control over models, costs, and rules, Kodus is an open-source, model-agnostic AI code review platform. The goal is to bring more context into the review process and surface important risks before merge.