Start with the behavior, not the test label
Suppose a page displays a list of articles and lets a visitor narrow it with a search field. The feature has several parts: matching text to article titles, responding to input, rendering results, and loading the page in a browser. A single test level does not need to cover all of them. Choose a level by asking what failure you want the test to detect and how much of the feature must run to detect it.
For the examples below, assume a plain JavaScript application running in a modern browser. A function filters an array of article objects, a component connects a text input to a results region, and the application is served locally for browser tests. The testing tools can vary; the important choices are which parts run together and which dependencies you control. Tests at different levels can overlap, but each should have a clear reason to exist.
Use unit tests for isolated rules
A unit test checks a small piece of behavior without mounting the page or making a network request. For example, pass the filter function three articles and the query "script"; assert that it returns the articles whose titles match. Add cases for an empty query, different letter casing, and no matches if those cases are part of the intended behavior. Give the function fixed input and check its output rather than depending on the current contents of the page.
This level is useful when a rule has meaningful edge cases or is reused elsewhere. A failing unit test can point directly to the matching logic, and it is usually straightforward to run repeatedly. Its limit is equally important: it cannot show that an input event calls the function, that results appear in the document, or that a visitor can use the feature. Avoid writing many unit tests for trivial implementation details while leaving the visible behavior untested.
Use integration tests for parts that must cooperate
An integration test checks a boundary between pieces. For the search feature, mount the component in a test document, provide a known set of articles, enter a query through the input, and inspect the rendered results. This exercises the event handler, filtering rule, and DOM update together. Check what a visitor could observe, such as result text or an empty-state message, rather than asserting that a particular internal method was called.
State the test environment clearly. A DOM simulation can support many component interactions, but it does not reproduce every browser behavior. If articles normally arrive from a request, provide a controlled response at the request boundary when the purpose is to test rendering. Use a separate test for request-handling behavior if it matters, including what the component displays when loading fails. Replacing every internal collaborator with a mock may make the test pass even when the pieces no longer work together.
Use end-to-end tests for a complete user path
An end-to-end test opens the served application in a real browser and follows a user path. For this feature, load the page, find the search field by its visible label, type a query, and check that the expected result appears while an unrelated one does not. One such test can catch wiring or configuration mistakes that narrower tests miss: a component may work in isolation yet never appear on the page.
Keep this layer focused on important paths rather than every possible input. Browser tests involve more of the application, so a failure may take longer to diagnose. Make the environment predictable by using known article data and waiting for an observable condition, such as results becoming visible, instead of an arbitrary delay. Decide whether a test should use a real service or a controlled response based on the boundary it is meant to verify. A browser test with a stubbed service can verify the page journey, but it cannot prove that the service itself works.
Build a small suite around distinct risks
For a first pass, write unit tests for the matching rules, an integration test for entering a query and seeing updated results, and an end-to-end test for finding an article from the loaded page. Add another test only when it covers a distinct risk: perhaps an integration test for a failed request or a browser test for the no-results state. This is a starting point, not a fixed quota. A feature with little isolated logic may need fewer unit tests; one with complex calculations may need more.
When a test fails, its level helps narrow the investigation. A unit failure suggests a rule is wrong. A passing unit test alongside a failing integration test points toward the connection between logic and interface. A passing integration test alongside a failing browser test suggests a problem in the served application or its environment. Keep test names tied to outcomes, use stable inputs, and review the suite as the feature changes. The goal is confidence in behavior without testing the same detail at every level.



