What Is Automated Testing? Why Developers Write Tests
An honest introduction to automated testing — unit, integration, and end-to-end tests, the testing pyramid, why tests pay back over time, and common excuses for skipping them.
What you'll learn
- ✓What an automated test actually is
- ✓The difference between unit, integration, and end-to-end tests
- ✓What the testing pyramid is — and why you should not invert it
- ✓Why tests pay back even though they look like extra work
- ✓Common excuses for skipping tests, and why they do not survive contact with real codebases
Prerequisites
- •Some experience writing code in any language
- •No prior testing experience required
Most developers write tests reluctantly the first time. By the third or fourth project, they refuse to work without them. This post explains why — what tests are, what they cost, and what they buy you.
The examples here are language-agnostic. The principles apply whether you write Python, JavaScript, Go, or anything else.
What an automated test is
An automated test is a piece of code that runs your other code and checks the result against expectations. If the result matches, the test passes. If not, it fails — loudly, with a useful message.
Pseudocode for the world’s smallest test:
function add(a, b) { return a + b }
test "add returns the sum" {
assert add(2, 3) == 5
}
That is the entire idea. The test runner finds your test functions, runs them, and reports which passed and which failed. You run the whole suite with one command, get a report in seconds, and learn whether your last change broke anything.
Manual testing — clicking through the app yourself — works for tiny projects. It does not scale. After ten features and a hundred edge cases, no human can check them all on every change. Automated tests can.
The three main kinds
Tests are usually classified by what they cover and how fast they are.
Unit tests
A unit test exercises a single function, class, or small module in isolation. It runs in milliseconds and has no external dependencies — no real database, no network, no filesystem if possible.
test "discount of 10% on $100 returns $90" {
assert applyDiscount(100, 0.10) == 90
}
You write the most unit tests because they are cheap to run and pin down behaviour precisely. When one fails, the failure points directly at the broken function.
Integration tests
An integration test checks that several pieces work together — your code plus a real database, plus a queue, plus the file system. It is slower (often hundreds of milliseconds) and more complex to set up.
test "creating a user persists them to the database" {
user = createUser({name: "Ada"})
saved = db.find(user.id)
assert saved.name == "Ada"
}
Integration tests catch the bugs that live in the seams between components — the misspelled column name, the off-by-one in a schema migration, the bad SQL nobody noticed.
End-to-end (E2E) tests
An E2E test drives the whole application from the outside — opens a browser, clicks through a flow, asserts something visible to a real user.
test "user can sign up and see the dashboard" {
visit "/signup"
fill "email", "ada@example.com"
fill "password", "hunter2"
click "Sign up"
expectVisible "Welcome, Ada"
}
E2E tests are slow (seconds or minutes), brittle (UI changes break selectors), and expensive to maintain. They are also the only way to catch certain whole-system bugs. You want a few. You do not want hundreds.
The testing pyramid
A useful mental model:
/\
/ \
/ E2E \ few, slow, full system
/------\
/ Int \ some, medium speed
/----------\
/ Unit \ many, fast, focused
/--------------\
The pyramid suggests:
- Many unit tests at the base. They are fast and pinpoint failures.
- Some integration tests in the middle. They cover the seams.
- A handful of end-to-end tests at the top. They cover the most important user flows only.
The classic antipattern is the ice cream cone — lots of slow E2E tests, almost no unit tests. Test runs take an hour, failures are vague, and nobody trusts them. Avoid it.
Why tests pay back
Writing the test is extra work upfront. The payoff comes later — and the payoff is large.
1. Refactoring stops being terrifying
Without tests, every refactor is a leap of faith. With tests, you change the code, run the suite, and either see green or get a precise list of what broke. Months-old code stops being “do not touch” and starts being malleable again.
2. Bugs get cheaper
A bug caught by a unit test costs minutes. A bug caught in a code review costs hours. A bug caught in production costs reputation, on-call time, and sometimes money. Tests move bugs left — earlier in the process, where they are cheap.
3. Documentation that cannot lie
A passing test is executable documentation. New teammates read tests to learn how a function is supposed to behave. Comments rot; tests fail when the code drifts away from them.
4. Confidence to ship
The biggest payoff is psychological. A team with a good test suite ships small changes often, with low fear. A team without one batches changes into nervous monthly releases. The first team moves much faster over a year.
Common excuses (and why they’re wrong)
People avoid writing tests for predictable reasons. Most do not hold up.
”We don’t have time to write tests.”
You do not have time not to. The hours saved on debugging and on the next refactor exceed the hours spent writing tests, usually within weeks. If a deadline is genuinely tight, write tests for the riskiest path and skip the trivial ones — but do not skip everything.
”My code is obviously correct.”
Every developer believes this about every piece of code they have just written. It is wrong roughly half the time. Tests are not for what you can see; they are for what you cannot.
”Tests slow down development.”
The first day with tests is slower. By the second month, you are faster, because you can change code without breaking other code. Untested codebases get progressively harder to change. Tested ones do not.
”I’ll add tests later.”
You will not. Code without tests rarely gets tests added retroactively, because the design that grew without tests is usually hard to test. Write them as you go.
”Tests are flaky and I don’t trust them.”
Flaky tests are a real problem — but they are a problem with those tests, not with testing itself. Investigate every flake, fix the root cause, and delete tests that cannot be made deterministic.
Try it yourself. Open any small project you have written. Pick one function that takes inputs and returns a value. Write three tests for it on paper: one for the happy path, one for an edge case (empty input, zero, negative number), and one for an error case. Notice how thinking about tests forces you to think about cases you might otherwise miss.
What makes a good test
A few habits separate useful test suites from painful ones.
- Test behaviour, not implementation. Assert on the output of a function, not on which private helper it called. Implementation changes; behaviour should not.
- One reason to fail. Each test should check one thing. A test that asserts ten facts gives you no signal about which one broke.
- Deterministic. No random data, no real time, no network calls. The same test should pass or fail the same way every run.
- Fast. A slow unit test is a bug. If a test needs a database, it is an integration test — put it in a different folder and run it separately.
- Readable. Future-you should be able to read the test and understand what behaviour is being protected. The classic pattern is arrange / act / assert.
test "removes inactive users" {
// arrange
users = [active("ada"), inactive("bob"), active("cleo")]
// act
result = removeInactive(users)
// assert
assert result == [active("ada"), active("cleo")]
}
What tests do not catch
A passing test suite is not a guarantee of correctness. Tests catch the cases you wrote them for. They do not catch:
- Cases you forgot to consider
- Bugs in requirements (“the spec is wrong”)
- Performance regressions (unless you specifically measure)
- Visual bugs and design mistakes
- Concurrency bugs that need many cores and the right timing
Code review, exploratory testing, monitoring in production, and load testing all stay important. Tests are one tool, not the only one.
Languages and frameworks
Every popular language has a mature test framework:
- Python: pytest, unittest
- JavaScript / TypeScript: Vitest, Jest, Mocha
- Go: the built-in
testingpackage - Java: JUnit, TestNG
- Ruby: RSpec, Minitest
- Rust: the built-in
#[test]attribute
The concepts transfer cleanly between them. Learn one well; the next is a weekend.
Try it yourself. Install pytest (pip install pytest) or Vitest (npm install -D vitest) in a small project. Write a file with one trivial passing test and one deliberately failing test. Run the suite and read the output. Notice how the failure points at the exact line — that signal is what makes tests valuable.
A realistic starting point
If you have never written tests on a project, do not try to test everything at once. A reasonable first sprint:
- Pick the three highest-risk functions in your codebase — the ones that handle money, user identity, or data that cannot be recovered.
- Write unit tests for the happy path of each.
- Add one test per bug you fix from this point on. The bug becomes a permanent regression test.
- Once the habit sticks, broaden coverage to whole modules.
You will not catch up by writing 500 tests in a week. You catch up by writing one test every time you touch the code.
Recap
You now know:
- An automated test is code that checks other code, runs in seconds, and reports failures clearly
- The three main kinds — unit, integration, and end-to-end — trade speed and coverage
- The testing pyramid suggests many unit tests, some integration tests, and few E2E tests
- Tests pay back through cheap refactoring, cheap bugs, executable docs, and ship confidence
- Most excuses for skipping tests collapse on contact with a real, growing codebase
- Tests catch what you write them for — not visual bugs, not perf regressions, not bad requirements
Next steps
The next step is picking a language and writing your first real test. If you work in Python, start with Pytest Basics. If you work in JavaScript, start with Vitest Basics. Both posts walk through install, your first passing test, and the small set of features you will use daily.
Questions or feedback? Email codeloomdevv@gmail.com.
Related articles
- Testing Pytest Basics: Writing Your First Python Tests
A practical introduction to pytest — installation, test discovery, the assert statement, parametrize, fixtures, and the command-line flags you will use every day.
- Testing Vitest Basics: Fast Unit Tests for JavaScript
A practical introduction to Vitest — why it exists, installation, describe and it, expect matchers, watch mode, and a small mock example for testing modern JavaScript.
- Testing Integration Testing: Test Real Dependencies, Not Mocks
Learn how to write effective integration tests that verify real database queries, API calls, and service interactions using testcontainers and proper test infrastructure.
- Testing Performance Testing: Load, Stress, and Spike Testing
Learn load testing, stress testing, and spike testing with k6 and Locust. Includes test scripts, result interpretation, and strategies for finding bottlenecks.