1. Home
  2. Blog
  3. Software Testing

Software Testing

Unit Testing vs Integration Testing: Differences, Examples and When to Use Each

Unit tests check one piece of logic alone. Integration tests check that real pieces work together. See both on the same small app, with tests we ran, and learn how much of each you need.

9 min read
Unit testing vs integration testing: differences, examples and when to use each

Key takeaways

  • A unit test checks one function or class in isolation, replacing dependencies with fakes.
  • An integration test checks that real components, such as your code and a database, work together.
  • Unit tests find logic bugs fast. Integration tests find bugs at the seams that mocks hide.
  • Write many unit tests, fewer integration tests and only a few end-to-end tests.

Teams argue about unit testing vs integration testing as if they had to choose one. They do not. The two answer different questions, and a healthy test suite needs both: “does my logic work?” and “do the parts work together?”

In this guide we test one small order feature at both levels, using only Python’s standard library, so you can run everything yourself. We ran it on Python 3.13, and all four tests pass.

The short answer

A unit test takes the smallest testable piece of code, usually a function or a method, and checks it on its own. Anything outside that piece, such as a database or a network call, gets replaced with a fake. So these tests are fast and pinpoint the exact line that broke.

An integration test connects two or more real parts and checks how they behave together. It might run your code against a real database, or call one of your services through its actual API. It is slower, but it tests something a unit test cannot: whether the assumptions each part makes about the others are true.

Unit testing vs integration testing side by side

Aspect Unit test Integration test
Scope One function or class Two or more real components
Dependencies Replaced with mocks or fakes Real, or close to real
Speed Milliseconds Milliseconds to minutes
Finds Logic errors and edge cases Wrong queries, contracts, configuration
When it fails Points to one small area Takes more investigation
Usually written by Developers Developers and QA engineers
Runs On every save and commit On every pull request

One small app to test

Our feature has three parts: a pure discount calculation, a repository that stores orders in a database, and a function that connects the two.

def discounted(price, percent):
    if not 0 <= percent <= 100:
        raise ValueError("percent must be 0-100")
    return round(price * (1 - percent / 100), 2)

class OrderRepo:
    def __init__(self, conn):
        self.conn = conn
        conn.execute("create table if not exists "
                     "orders(id integer primary key, total real)")

    def save(self, total):
        sql = "insert into orders(total) values (?)"
        return self.conn.execute(sql, (total,)).lastrowid

    def total_of(self, order_id):
        sql = "select total from orders where id=?"
        return self.conn.execute(sql, (order_id,)).fetchone()[0]

def place_order(repo, price, percent):
    return repo.save(discounted(price, percent))

Notice that place_order receives the repository as an argument. This small design choice, called dependency injection, is what makes it easy to swap in a fake during unit tests.

A unit test

The first two tests check pure logic. The third checks place_order with a Mock instead of a real repository, so no database is involved.

import unittest
from unittest.mock import Mock

class UnitTests(unittest.TestCase):
    def test_discount(self):
        self.assertEqual(discounted(200, 10), 180.0)

    def test_bad_percent(self):
        with self.assertRaises(ValueError):
            discounted(200, 150)

    def test_place_order_saves_total(self):
        repo = Mock()
        repo.save.return_value = 7
        self.assertEqual(place_order(repo, 200, 10), 7)
        repo.save.assert_called_once_with(180.0)

The mock lets us ask a precise question: did place_order call save with the discounted total? It says nothing about whether save really works. That is a limit of the test, and it is intentional. Picking good input values matters too, and boundary value analysis tells you exactly which percentages to try, such as 0, 100 and 101.

An integration test

This test uses a real, in-memory SQLite database, so the actual SQL runs.

import sqlite3

class IntegrationTests(unittest.TestCase):
    def test_order_is_stored(self):
        repo = OrderRepo(sqlite3.connect(":memory:"))
        order_id = place_order(repo, 200, 10)
        self.assertEqual(repo.total_of(order_id), 180.0)

if __name__ == "__main__":
    unittest.main(verbosity=2)

Here is the output of the whole file from our run:

test_order_is_stored (__main__.IntegrationTests.test_order_is_stored) ... ok
test_bad_percent (__main__.UnitTests.test_bad_percent) ... ok
test_discount (__main__.UnitTests.test_discount) ... ok
test_place_order_saves_total (__main__.UnitTests.test_place_order_saves_total) ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.009s

OK

Tip: SQLite in memory is great for learning, but test against the same database engine you use in production when you can. Engines differ in types and SQL behaviour, and a container makes that easy. See our Docker Compose tutorial.

Why you need both

Imagine someone renames the column total to amount in the create table statement but forgets the insert query. All three unit tests still pass, because none of them touches SQL. The integration test fails immediately.

Now imagine a bug in the discount formula that appears only at exactly 100 percent. An integration test could catch it, but you would need a database round trip for every edge case. A unit test can check dozens of values in milliseconds.

That is the whole argument. Unit tests give you cheap depth on logic, and integration tests give you confidence at the seams. Most production bugs that survive a strong unit suite live in those seams: database schemas, API formats, permissions, time zones and configuration. When a service talks to another over HTTP, knowing the HTTP status codes helps you write better checks.

The test pyramid

The test pyramid is a simple guide to how many tests to write at each level.

Level Quantity Example
End-to-end A few A browser completes a real checkout
Integration Some Order service writes to a real database
Unit Many Discount calculation returns the right value

The higher the level, the more realistic the test, but also the slower and more fragile. Do not treat the shape as a strict rule, though. A thin service that mostly reads and writes a database has little logic to unit test, so it may reasonably have more integration tests than unit tests. For the top of the pyramid, read our end-to-end testing best practices.

Running both in CI

Order your pipeline by speed, so developers get the fastest feedback first.

  1. Lint and unit tests on every push. Keep them under a couple of minutes.
  2. Integration tests on every pull request, with services such as a database started as containers.
  3. End-to-end tests before merging or before release, covering only the critical journeys.

If a stage fails, the later ones do not run, which saves time and money. New to pipelines? Start with what is CI/CD and then the GitHub Actions CI/CD guide.

Mocks, stubs and fakes

People often call every replacement a “mock”, but the differences help you choose the right one. Together, they are known as test doubles.

Double What it does Example
Stub Returns a fixed answer An exchange rate service that always returns 83
Mock Records calls so you can check them Our repo.save.assert_called_once_with(180.0)
Fake A simple working version An in-memory list used as a repository

Prefer stubs and fakes where you can, because they check results rather than the exact sequence of calls. Heavy use of mocks ties a test to how the code is written, so a harmless refactor can break it. Use a mock when the call itself is the behaviour you care about, such as confirming that an email was sent.

Which test should you write?

When you are adding a test, these three questions usually settle it:

  1. Is it pure logic, with inputs and outputs only? Write a unit test.
  2. Could it break because of something outside your code, such as SQL, a schema or a service’s response format? Write an integration test.
  3. Would a customer lose money or get blocked if a whole journey failed? Add one end-to-end test as well.

When a bug reaches production, write the test at the lowest level that would have caught it. That keeps your suite fast and stops the same bug returning.

Common mistakes

  • Mocking everything. If every dependency is fake, tests can stay green while the real system is broken.
  • Calling slow tests “unit tests”. A test that needs a running database is an integration test, so keep the suites separate.
  • Testing implementation details. Tests that check private steps break on every refactor. Test behaviour instead.
  • Shared test data. Integration tests that depend on each other’s rows fail randomly. Give each test its own data.
  • Only chasing coverage. A high percentage does not prove the parts work together.

Final thoughts

Use unit tests for logic and edge cases, and integration tests for everything that crosses a boundary. Start by adding one integration test around your most important database or API call, because that single test often catches what hundreds of mocks cannot.

Frequently asked questions

What is the main difference between unit and integration testing?

Unit testing checks one piece of code in isolation with dependencies faked. Integration testing checks that real components work correctly together.

Which should be done first?

Unit tests usually come first because they are written alongside the code and run fastest. Integration tests follow once the parts can be connected.

Do unit tests use a database?

Normally no. A database is replaced by a mock or fake. A test that talks to a real database is generally treated as an integration test.

Can integration tests replace unit tests?

Not well. They are slower and harder to diagnose, and covering every edge case through them is expensive. Use each for what it does best.

How many integration tests should I write?

Cover the important paths across each boundary, such as every key query and external call. Leave edge cases of pure logic to unit tests.

Keep learning

Work with Stack Quora

Need help with testing, WordPress or SEO?

We help teams ship better software and grow their sites. Tell us what you are working on and we will suggest a clear next step.

Keep reading

More guides on testing, AI & modern development

Enjoyed this article? Pick a topic and jump straight into our newest tutorials.

Artificial Intelligence

LLM Structured Output Explained: Getting Reliable JSON From AI Models

LLM structured output explained with validation we ran in Pydantic: JSON mode and schemas, constrained decoding, the ways model output…

Read article →
DevOps

SSH Keys Explained: How They Work, ssh-keygen, Ed25519 and Safe Habits

SSH keys explained with keys we generated: public vs private keys, ssh-keygen, Ed25519 vs RSA, fingerprints, passphrases, ssh-agent, authorized_keys, config…

Read article →
DevOps

HTTP Caching Explained: Cache-Control, ETag and 304 Responses, With a Live Test

HTTP caching explained with a small server we ran: Cache-Control directives, max-age, no-cache vs no-store, ETag revalidation and 304 responses,…

Read article →
Test Automation

API Contract Testing Explained: Catch Breaking Changes Before They Ship

API contract testing explained with a schema check we ran: what a contract is, consumer-driven contracts, Pact, OpenAPI, which changes…

Read article →
Software Testing

Risk-Based Testing Explained: Score Risks, Build a Matrix and Test What Matters

Risk-based testing explained step by step: identifying risks, scoring likelihood and impact, building a risk matrix, mapping risk to test…

Read article →
Databases & SQL

SQL Constraints Explained: NOT NULL, UNIQUE, CHECK and FOREIGN KEY, Tested

SQL constraints explained with tests we ran: NOT NULL, UNIQUE, CHECK, DEFAULT, PRIMARY and FOREIGN KEY, the NULL loopholes, SQLite's…

Read article →
Databases & SQL

SQL Date Functions Explained: Adding, Comparing and Grouping Dates Without Bugs

SQL date functions explained with queries we ran: adding intervals, month-end surprises, date differences, the BETWEEN trap, grouping by day,…

Read article →
Software Development

Python asyncio Explained: async, await, gather and TaskGroup With Real Timings

Python asyncio explained with code we timed: coroutines, await, gather, TaskGroup, error handling, timeouts, limiting concurrency and the blocking-call mistake…

Read article →
Software Development

Debounce vs Throttle in JavaScript: How They Work, When to Use Each, Tested

Debounce vs throttle in JavaScript explained with timings we measured in Node.js: how each works, simple implementations, leading and trailing…

Read article →
Software Development

Python Context Managers Explained: The with Statement, contextlib and Real Uses

Python context managers explained with code we ran: how with works, __enter__ and __exit__, contextlib.contextmanager, suppress, ExitStack, database transactions and…

Read article →
Artificial Intelligence

LLM Temperature and Top-p Explained: How AI Models Choose Their Next Word

LLM temperature and top-p explained with a simulation we ran: how probabilities are reshaped, what top-p keeps, recommended settings by…

Read article →
DevOps

YAML Explained: Syntax, the Norway Problem and Other Traps We Tested

YAML explained with a parser test: basic syntax, lists and maps, the Norway problem, versions turning into floats, octal zip…

Read article →
DevOps

Cron Jobs Explained: Syntax, Real Schedules and the Mistakes That Break Them

Cron jobs explained with schedules we verified: the five fields, operators, real examples, the day-of-month OR day-of-week trap, environment problems,…

Read article →
Software Testing

Bug Life Cycle in Software Testing: Every Status, Who Owns It and Common Mistakes

The bug life cycle explained: every defect status from New to Closed, who owns each step, rejected, deferred and reopened…

Read article →
Test Automation

Property-Based Testing in Python: Hypothesis Finds the Bugs Your Examples Miss

Property-based testing explained with Hypothesis tests we ran: properties vs examples, strategies, shrinking, the real bugs it found in our…

Read article →
Databases & SQL

SQL Pagination Explained: OFFSET vs Keyset, With a Million-Row Test

SQL pagination explained with a million-row test we ran: LIMIT and OFFSET, why deep pages get slow, keyset (cursor) pagination,…

Read article →
Databases & SQL

SQL EXISTS vs IN: Differences, the NOT IN NULL Trap and Performance

SQL EXISTS vs IN explained with queries we ran: how each works, why NOT IN returns no rows when a…

Read article →
Software Development

Python Logging Explained: Levels, Handlers, Formatters and Mistakes to Avoid

Python logging explained with code we ran: levels, basicConfig quirks, named loggers, handlers and formatters, logger.exception, lazy formatting, duplicate logs…

Read article →
Software Development

JavaScript Map vs Object: Key Differences, Pitfalls and a Benchmark

JavaScript Map vs Object explained with code we ran in Node.js: key types, ordering, the prototype trap, JSON, size, WeakMap…

Read article →
Software Development

Python Type Hints Explained: Annotations, mypy and the Errors It Catches

Python type hints explained with a real mypy run: basic annotations, Optional and unions, TypedDict, Literal, Protocol, generics, Callable, strict…

Read article →
Artificial Intelligence

LLM Tokens Explained: How AI Models Read Text, Count Costs and Fill Context Windows

LLM tokens explained with a tokenizer we ran: how text is split, why counts differ by language and tokenizer, context…

Read article →
DevOps

Semantic Versioning Explained: MAJOR.MINOR.PATCH, Ranges and Release Rules

Semantic versioning explained with code we ran: MAJOR.MINOR.PATCH rules, pre-releases, correct version sorting, caret and tilde ranges, 0.x versions, lockfiles…

Read article →
DevOps

Git Reset vs Revert: How to Undo Commits Safely, With a Tested Example

Git reset vs revert explained with a repository we built: soft, mixed and hard reset, revert commits, reverting merges, recovering…

Read article →
Software Testing

The Test Pyramid Explained: Layers, Ratios and When to Break the Shape

The test pyramid explained: unit, integration and end-to-end layers, sensible ratios, the ice cream cone anti-pattern, the testing trophy and…

Read article →
Test Automation

pytest Fixtures Explained: Scope, yield Teardown, conftest.py and Factories

pytest fixtures explained with a suite we ran: how fixtures work, scopes, yield teardown, conftest.py, factory fixtures, parametrized fixtures, built-ins…

Read article →
Databases & SQL

SQL Upsert Explained: INSERT ON CONFLICT, MERGE and the REPLACE Trap

SQL upsert explained with queries we ran: INSERT ON CONFLICT DO UPDATE and DO NOTHING, the excluded row, conditional updates,…

Read article →
Databases & SQL

SQL LIKE Operator Explained: Wildcards, Case Sensitivity and Performance

The SQL LIKE operator explained with queries we ran: % and _ wildcards, escaping, NOT LIKE and NULL, case sensitivity…

Read article →
Artificial Intelligence

AI Hallucinations Explained: Why Language Models Make Things Up and How to Reduce It

AI hallucinations explained: what they are, why language models produce them, common types, how to reduce them with grounding and…

Read article →
Test Automation

Mocking in Python: unittest.mock, patch and the Mistakes That Make Tests Lie

Mocking in Python with unittest.mock, tested: Mock and return_value, side_effect, patching where a name is used, spec and autospec, mocking…

Read article →
Software Testing

Code Coverage Explained: Line vs Branch Coverage and What 100% Really Means

Code coverage explained with a real coverage.py run: line, branch and condition coverage, reading a report, why 100% coverage can…

Read article →
Artificial Intelligence

What Are AI Agents? How They Work, With a Simple Agent Loop You Can Run

What are AI agents? A plain guide to the agent loop, tools, memory and guardrails, with a small Python loop…

Read article →
Test Automation

TDD vs BDD: Key Differences, One Feature Built Both Ways and When to Use Each

TDD vs BDD explained with one discount feature built both ways: red-green-refactor with code we ran, Given-When-Then scenarios, tools, trade-offs…

Read article →
Software Testing

User Acceptance Testing (UAT): Process, Checklist and Real Examples

User acceptance testing explained: who runs it, how it differs from QA, a seven-step process, a UAT test case template,…

Read article →
Artificial Intelligence

What Are Embeddings? Vectors and Cosine Similarity Explained Simply

What are embeddings? A plain-language guide to vectors, cosine similarity and semantic search, with a small example we ran, common…

Read article →
Test Automation

Flaky Tests: What Causes Them and How to Fix Them for Good

Flaky tests explained with demos we ran: the main causes, how to confirm and diagnose one, proper fixes for timing,…

Read article →
Software Testing

Load Testing vs Stress Testing: Differences, Metrics and k6 Examples

Load testing vs stress testing explained: what each proves, spike and soak tests, the metrics that matter, why averages mislead,…

Read article →