Unit Testing: What Software Testers Need to Know
Learn what unit tests cover, how they differ from integration tests, and how to write useful tests that stay fast, isolated, and maintainable.
A unit test checks one software component or method against expected behavior, usually without relying on external infrastructure such as a database, filesystem, or network. Good unit tests are fast, isolated, repeatable, self-checking, and economical to maintain. They help catch regressions and make behavior explicit, but they do not prove that connected components work together or that the whole application is correct.
1. What is unit testing?
Unit testing is the practice of checking a small, defined piece of software by supplying inputs and verifying observable results. A “unit” might be a function, method, class, or another unit of work. Its exact boundary depends on the design and context of the project.
A useful unit test answers a focused question: given this scenario, does this code produce the expected result or behavior? For example, a pricing function might return the expected total for a valid order, reject an invalid quantity, or apply a discount according to a rule.
Tests also serve as executable documentation. A clear test name and its assertions show what a behavior means, and rerunning the test after code changes helps reveal regressions. Microsoft’s guidance describes these benefits alongside test design practices in its unit testing best practices.
2. How is a unit test different from an integration test?
The main distinction is scope. A unit test checks one unit of behavior in isolation. An integration test checks whether two or more components work together, often including infrastructure such as a database or network service. Both are needed because a collection of passing unit tests cannot establish that real dependencies and boundaries work together.
| Aspect | Unit test | Integration test |
|---|---|---|
| Question | Does this unit behave correctly for this scenario? | Do these components cooperate correctly? |
| Scope | One component or method, as defined by the team | Two or more components and their boundary |
| Dependencies | Usually replaced or controlled to isolate behavior | May use real infrastructure or representative services |
| Typical concern | Business rule, validation, transformation | Persistence mapping, API contract, service configuration |
For instance, a unit test can check that a function maps an order to a payment request with the expected amount. An integration test can check that the application sends that request through its configured HTTP client and handles the service response correctly. The unit test does not cover DNS, credentials, network failures, or the external service’s behavior.
Keep infrastructure checks in integration or functional tests where they can exercise the real boundaries. Microsoft’s .NET testing guidance makes this distinction between individual units and tests that include multiple components and infrastructure: .NET testing overview.
3. What makes a good unit test?
- Fast: It should be practical to run frequently while developing and in CI.
- Isolated: Its result should focus on the behavior under test instead of unrelated services or machine state.
- Repeatable: With the same code and inputs, it should produce the same result. Avoid dependence on current time, random values, shared mutable state, or the outside network unless controlled.
- Self-checking: The runner should report pass or failure without a person interpreting logs manually.
- Named clearly: Make the behavior, scenario, and expected outcome recognizable from the test name.
- Economical to maintain: Assert meaningful outcomes without coupling the test to incidental implementation details.
Microsoft recommends these qualities and suggests naming patterns that identify the method, scenario, and expected behavior. One example shape is Method_Scenario_ExpectedBehavior; adapt naming to the language and conventions already used by the project.
4. A practical process for writing unit tests
- Choose a behavior that matters. Start with a rule, boundary condition, or bug-prone scenario rather than trying to test every line mechanically.
- State the scenario and expectation. Write down the input or condition and the observable result that should follow.
- Keep the boundary narrow. Avoid opening databases, files, or network connections in a unit test. If the behavior intrinsically depends on such a boundary, decide whether the test is really an integration test.
- Arrange, act, assert. Set up the scenario, invoke the behavior, and assert the result. Keep setup small so the reason for the test remains obvious.
- Cover meaningful cases. Include ordinary input, boundary values, invalid input where relevant, and failure behavior that callers need to handle.
- Run the test alone and with the suite. A test that passes alone but fails in a suite may depend on ordering or shared state. A test that fails alone may have hidden setup supplied by another test.
- Keep integration checks in the plan. Add tests for the real component boundaries that unit tests intentionally replace.
For example, a behavior table for a quantity validator could include a positive quantity, the minimum permitted value, zero, and a negative value. Each row should assert a public result or documented error, not the exact private sequence of internal calls.
5. Test doubles: fake, stub, and mock
A test double is a substitute used to isolate a unit from a dependency. The terms fake, stub, and mock are not used consistently across all tools and testing literature, so define them in the context of the framework and team.
- Stub: In classic test-double terminology, supplies predetermined data or responses.
- Mock: In classic terminology, records or verifies interactions that are part of the expected behavior.
- Fake: A working alternative implementation, often simpler than the real dependency, such as an in-memory repository.
Microsoft documents both this classic vocabulary and broader use in .NET guidance. The practical question is what the substitute does and why it isolates the behavior. Use a double when the interaction itself matters or when a dependency makes a unit test slow or nondeterministic. Avoid verifying every internal call: that can make tests brittle when implementation changes without changing behavior.
6. Coverage: what it tells you and what it does not
Code coverage measures how much code was exercised while tests ran. It can help identify untested areas, but a coverage percentage does not show whether assertions are meaningful, whether important cases were considered, or whether the application works end to end. High coverage alone is not a quality guarantee.
Use coverage as a diagnostic signal: inspect uncovered code, decide whether it represents important behavior, and improve tests where the risk justifies it. Do not select a numeric target as a substitute for reviewing test usefulness. Microsoft’s coverage guidance explicitly cautions against treating percentage alone as a measure of test quality.
7. Choose a framework for your language and workflow
There is no universal framework choice in the sources cited here. Compare options against language and ecosystem fit, runner and IDE support, assertion and fixture features, and compatibility with the project’s CI workflow. Confirm current compatibility in the framework’s own documentation before adopting it.
- .NET: Microsoft’s overview lists MSTest, NUnit, TUnit, and xUnit.net. It distinguishes the test platform, which discovers and runs tests, from the framework APIs used to author them. The
dotnet testcommand provides a CLI path for test projects and scripted CI/CD workflows. See the official .NET testing overview. - C++: GoogleTest is Google’s C++ testing and mocking framework. Its primer covers independent, repeatable tests, suites, and assertions. It can support kinds of testing beyond unit tests.
- Python: pytest 8.2 fixture documentation describes named, reusable setup dependencies that can be composed, scoped, and parametrized, including teardown. A fixture error means a test may not have been attempted because setup failed.
These are examples for their ecosystems, not a ranking. If a project already has a working runner and CI path, consistency and compatibility may matter more than switching for a feature that the team does not need.
8. Common unit testing problems and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| Test passes on one machine but fails elsewhere | Dependence on local time zone, locale, filesystem state, environment variables, or external services | Control the input and environment, isolate infrastructure, and move real boundary checks into integration tests. |
| Test results change between runs | Randomness, current time, shared state, order dependence, or concurrency | Inject or freeze time and random sources, reset state, and ensure each test can run independently. |
| Suite is slow | Unit tests are opening databases, starting browsers, or making network calls | Keep unit scope narrow; use controlled doubles where appropriate and reserve infrastructure checks for integration tests. |
| Many tests break after a harmless refactor | Assertions are coupled to private implementation details or incidental call sequences | Assert externally observable behavior and retain interaction assertions only where the interaction is part of the contract. |
| Coverage is high but bugs still escape | Lines execute without meaningful assertions, or integration boundaries are untested | Review scenario quality and expected outcomes; add integration or functional checks for connected components. |
| Fixture/setup error is reported | Required setup failed, so the test body could not run | Read the setup error first, check fixture scope and dependencies, and keep shared setup small. pytest documents setup errors separately from assertion failures. |
| Test runner discovers no tests | Project, framework, or runner configuration is missing or incompatible | Check the official framework and platform instructions, test naming/discovery conventions, target framework, and CI command. |
9. Performance, reliability, and maintenance
Fast unit tests shorten the feedback loop and make it practical to rerun the suite after changes. Reliability depends on deterministic inputs, independent setup, and clear failure output. Keep expensive operations and unstable dependencies outside unit boundaries where practical, while maintaining integration coverage for those dependencies.
Maintainability comes from testing behavior rather than implementation trivia. Prefer a compact number of tests that explain distinct cases, use descriptive names, and make failures local and understandable. When behavior changes intentionally, update the expectation and its documentation; when the test breaks unexpectedly, treat that as a signal to investigate before changing assertions.
Unit testing itself does not require a paid service. Framework, runner, and CI costs depend on the project’s selected tools and infrastructure; the cited sources do not establish comparative cost or productivity figures.
10. ScreenshotNeo for visual checks in a test workflow
Unit tests cover code behavior. When a test workflow also needs a rendered web page capture—for a visual check, review artifact, or page snapshot—a screenshot API is a separate tool for that job. ScreenshotNeo is a website screenshot API and MCP server from Yorker Media. Its one-call API returns PNG, JPEG, WebP, or PDF, and the API accepts parameter names used by other screenshot APIs to make switching easier.
The call below is a runnable cURL example; replace the key with one from your account. See the ScreenshotNeo API documentation for request options and response details.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Or call the same endpoint from Python:
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
And from Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const bytes = new Uint8Array(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));
ScreenshotNeo offers full-page capture with lazy images loaded, CSS-selector element capture, dark mode, 12 device presets and custom viewports, retina scale, PDF settings, HTML/CSS capture, custom CSS and JavaScript, click and wait actions, selector hiding, request and resource blocking, custom headers, cookies, user agent and Authorization, timezone and geolocation, transparent backgrounds, resizing, configurable caching, signed image links, async jobs with signed webhooks, bulk capture up to 100 URLs per call, a usage API, and an OpenAPI specification. Cookie/consent banners, newsletter popups, and chat widgets can be removed before capture; each cleanup step can be turned off. Responses identify page verdict and billing status in headers.
11. Or skip the browser setup
ScreenshotNeo removes cookie banners, popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed, and response headers report the page verdict and billing status. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
The free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; higher plans are Growth at $15 for 15,000, Pro at $39 for 60,000, Scale at $99 for 250,000, and Business at $249 for 1,000,000. Yearly billing gives two months free, and every feature is available on every plan.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Sign up for 1,000 free screenshots a month with no card.
12. Frequently asked questions
Should every function have a unit test?
Not necessarily. Focus on meaningful behavior and risk. A test count or coverage percentage does not by itself indicate that important outcomes are protected.
Can unit tests prove that an application is correct?
No. They provide evidence about the units and scenarios they exercise. Integration and functional checks cover behavior across connected components and infrastructure.
Are mocks required for unit testing?
No. Use a test double when it helps isolate the behavior or verify an important interaction. Simple deterministic code may need no double at all.
Is a test with a database always an integration test?
It usually exercises an infrastructure boundary and is commonly treated as an integration test. Teams may define layers differently, so make the test’s scope and purpose clear.
Which framework should a team choose?
Choose one that fits the project language, existing workflow, runner and IDE support, and the features the team will use. Verify current compatibility in official documentation.
Further reading
For a deeper treatment of test goals, coverage, test quality, mocking, and anti-patterns, see Unit Testing Principles, Practices, and Patterns by Vladimir Khorikov, published by Manning in January 2020. The publisher’s listing describes it as an intermediate-to-advanced book.


