Reviewing Code Generated by AI
Igor Brito | Jan 15, 2026
Building reliable frontends by testing user behavior, not implementation details.
For years, frontend developers have repeated the same excuse:
“We don’t have time to write tests.”
In the past, that was understandable. Testing used to be slow, repetitive, and disconnected from the creative process of building user interfaces.
You had to set up complex mocks, deal with brittle selectors, and rewrite tests every time the UI changed slightly. Today, things are very different.
Artificial Intelligence has made testing faster, smarter, and focused on what truly matters: user behavior. With the right architecture, it is now easier than ever to write code that is both testable and resilient.
This article examines how this shift occurs. You will learn how to design testable React code, how to test real user behavior, and how to use AI to remove friction from the testing process.
The problem often starts with how we structure our components. Many developers write React components that handle everything in one place: fetching data, managing state, running side effects, and rendering the UI.
That approach seems simple, but quickly becomes a trap. The result is tightly coupled code where testing one part requires pulling in everything else.
Refactoring becomes risky, and tests become fragile because they depend on implementation details rather than behavior. The foundation of reliable testing is the Separation of Concerns.
Read more: Beyond “Vibe Coding”: Engineering with AI and Cursor
A well-designed frontend separates responsibilities into three layers:
| Layer | Role | Example | Test Type |
|---|---|---|---|
| Business Logic | Core rules and calculations | validation, formatting, API calls | Unit tests |
| Application Logic | Connects data and UI | custom hooks, state handlers | Integration tests |
| UI (Presentation) | What users see and interact with | components, layout | Integration and E2E tests |
Each layer can be tested independently.
When business logic is isolated from React, you can test it through simple input and output. When application logic lives in hooks, you can verify it through UI behavior instead of internal state.
When your UI is accessible and predictable, your tests can focus entirely on what users experience, not on how the code is written.
This is the single most important idea in frontend testing. Many test suites fail because they check internal details instead of external behavior.
Developers often assert variable values or hook calls, which tie tests directly to implementation details. As soon as the internal structure changes, these tests break even if the user experience remains identical.
Behavior-driven testing (BDT) takes the opposite approach. Instead of testing how the app works, you test what the user sees and does.
// Breaks when internal state names change
expect(screen.getByTestId('modal')).toHaveClass('open')Code language: JavaScript (javascript)
// Fails only if the visible behavior changes
userEvent.click(screen.getByRole('button', { name: /open modal/i }))
expect(screen.getByRole('dialog', { name: /user settings/i })).toBeVisible()Code language: JavaScript (javascript)
The second test is behavior-based. It will remain valid after refactors and fail only when the actual user behavior changes.
When you test behavior, you validate the contract between your interface and your users. You are not checking implementation details but ensuring that the user experience works as intended.
Behavior-driven testing also serves as documentation. Each test tells a story about how the user interacts with the product.
For example:
These are not just test descriptions. They are behavioral guarantees that define how your application should work.
As long as these behaviors remain consistent, you can refactor with confidence.
To test user behavior effectively, follow these three essential rules:
Find elements by visible text or accessibility attributes:
screen.getByRole('textbox', { name: /email/i })
screen.getByRole('button', { name: /submit/i })
screen.getByText(/registration completed successfully/i)Code language: JavaScript (javascript)
Avoid using data-testid unless necessary. Users never see test IDs, so behavior-driven tests should not rely on them.
Use user-event instead of fireEvent to simulate natural interactions:
await userEvent.type(screen.getByLabelText(/email/i), 'user@example.com')
await userEvent.click(screen.getByRole('button', { name: /submit/i }))
Code language: JavaScript (javascript)
This approach ensures that tests behave the same way real users would.
Check what the user perceives, not what the code stores:
expect(screen.getByText(/invalid cpf/i)).toBeVisible()
Code language: JavaScript (javascript)
Avoid asserting internal states such as expect(isValidCpf).toBe(true).
Users see messages, not variables.
Read more: Reviewing Code Generated by AI
Accessibility is not only about inclusion; it is also a powerful testing strategy. When you use accessibility roles and labels, your tests become more reliable and future-proof.
Queries such as: “getByRole, getByLabelText, and getByPlaceholderText” reflects on how assistive technologies interpret your interface. By relying on these queries, you guarantee that your components remain discoverable both for screen readers and for automated tests.
This approach encourages teams to think in terms of semantics instead of structure. Instead of targeting hidden attributes or arbitrary classes, you query meaningful roles like button, dialog, or textbox.
These roles rarely change even when the DOM structure does, which keeps your tests stable through refactors. Accessibility-based testing improves both product usability and code resilience.
It creates tests that are more human-readable, more meaningful, and aligned with real-world usage.
Imagine a small registration form with the following fields:
000.000.000-00(00) 00000-0000 or (00) 0000-0000Each behavior becomes a test case. You are not testing the logic directly; you are testing how the interface responds when a user interacts with it.
TDD (Test-Driven Development) fits naturally with a behavior-driven approach. You start by describing the behavior before writing any code.
describe('User Registration Form', () => {
it('shows an error if user is under 18', ...)
it('formats CPF and phone automatically', ...)
it('disables submit while submitting', ...)
it('shows success message after valid submission', ...)
})
Code language: PHP (php)
These tests will initially fail. That is the expected part of the process.
As you implement the logic, tests start passing one by one, confirming that the app behaves exactly as designed.
Traditional TDD required developers to write every test and implementation manually. AI now accelerates that cycle significantly.
Describe a feature in natural language:
“A registration form with name, CPF, phone, birthdate, and email.
Validate fields, disable submit during loading, and show success or error messages.”
From this description, along with well-defined rules and context, AI can generate:
You can then refine or expand those tests while focusing on clarity and correctness instead of setup code.
This new way of testing allows developers to invest their time in what matters.
When you describe behavior clearly, AI can create accurate tests that reflect real user interactions.
Clear and behavior-oriented descriptions produce higher-quality results. Here is a simple and effective prompt structure:
AI can generate a complete test suite from this description, along with pre-defined test rules. You can then refine it manually, adding accessibility and edge cases.
This workflow combines the best aspects of TDD and modern automation, providing both speed and precision.
That excuse is gone. With good architecture and AI-powered tools, testing is faster and more reliable than ever. You can:
Testing is no longer an obstacle. It is a safeguard for quality and consistency.
TDD was never about writing tests. It was about writing better software.
AI brings that philosophy back to life.
By focusing on user behavior, frontend developers can create meaningful tests, prevent fragile implementations, and ship features with complete confidence.
Behavior is the ultimate contract between your code and your users. When you test behavior, you are not only validating your UI. You are proving that your product keeps its promise.
Passionate about building high-performance, scalable, and maintainable front-end applications, I have nearly 7 years of experience specializing in the React ecosystem. Expertise with React, Next.js, TypeScript, React Native, Clean Architecture, Micro Frontends, Monorepos, TDD (Jest, Cypress, RTL), NodeJS, NestJS, ExpressJS, MongoDB, Postgres, relational and non-relational databases.