How to Write Your First Unit Test
SkillVeris Team
Engineering Team

A unit test is a small automated check that verifies one piece of code, usually a single function, produces the expected output for a given input.
In this guide, you'll learn:
- The Arrange-Act-Assert pattern structures every test: set up data, run the code, then check the result.
- A test runner like Jest or Vitest provides the test() and expect() functions and reports pass or fail.
- Good unit tests are fast, isolated, and deterministic, so they give the same result every run.
- Test behaviour and edge cases, not implementation details, so tests survive refactoring.
1What Is a Unit Test?
A unit test is a small, automated check that verifies one unit of code — typically a single function — behaves correctly. You give the function an input, run it, and assert that the output matches what you expect. If it does, the test passes; if not, it fails and tells you exactly what went wrong.
Unit tests are the foundation of automated testing because they are fast and precise. When one fails, it points at a specific function, not a vague area of the app, so you spend less time hunting and more time fixing. They also act as living documentation of how your code should behave.
2Why Write Unit Tests?
Testing feels like extra work until the first time a test catches a bug you would otherwise have shipped. The payoff shows up across the life of a project.
- Catch bugs early, when they are cheapest to fix.
- Refactor with confidence — passing tests confirm you did not break anything.
- Document behaviour: a test shows exactly how a function is meant to be called.
- Faster feedback than manually clicking through the app each time.
- Fewer regressions as the codebase and team grow.
🔑Tests Are a Safety Net
The real value of unit tests appears when you change code later. A green suite tells you your change is safe; a red one tells you precisely what broke.
3Choosing a Test Runner
A test runner is the tool that finds your tests, runs them, and reports results. In the JavaScript world, Jest and Vitest are popular choices, and Node.js now ships with a built-in test runner too. Installing one gives you the global test() and expect() functions your tests are written with.
- npm install --save-dev jest // install a test runner
- // package.json scripts:
- "scripts": { "test": "jest" }
- npm test // run all tests
4The Arrange-Act-Assert Pattern
Nearly every good unit test follows the same three-step shape. Arrange sets up the inputs and any needed state, Act runs the code under test, and Assert checks that the result is what you expected. Keeping tests in this order makes them predictable and easy to read.
- // the function under test
- function add(a, b) { return a + b }
- test('add sums two numbers', () => {
- const a = 2, b = 3 // Arrange
- const result = add(a, b) // Act
- expect(result).toBe(5) // Assert
- })
One Assertion Focus per Test
A test should verify one behaviour. When it fails, the name and single focus tell you immediately what broke. Splitting distinct behaviours into separate tests beats cramming many unrelated assertions into one.
5Assertions and Matchers
The expect() function pairs with matchers — methods that describe the check you want. toBe compares primitives, toEqual compares object contents, and others handle truthiness, errors, and more. Choosing the right matcher makes failures clear.
- expect(sum).toBe(5) // strict equality for primitives
- expect(user).toEqual({ id: 1 }) // deep equality for objects
- expect(list).toContain('apple') // membership
- expect(isValid).toBe(true) // booleans
- expect(() => parse('')).toThrow() // expects an error
⚠️toBe vs toEqual
Use toBe for numbers, strings, and booleans, but toEqual for objects and arrays. toBe checks reference identity, so two equal-looking objects fail toBe but pass toEqual.
6Testing Edge Cases
A single happy-path test is a start, but real confidence comes from covering the boundaries — the empty inputs, the zeros, the unexpected types. These edge cases are where bugs hide, so a good test file includes several tests for one function, each probing a different condition.
- test('add handles negatives', () => expect(add(-1, -1)).toBe(-2))
- test('add handles zero', () => expect(add(0, 5)).toBe(5))
- test('add handles decimals', () => expect(add(0.1, 0.2)).toBeCloseTo(0.3))
7Best Practices
A handful of principles separate helpful tests from brittle ones.
- Keep tests fast, isolated, and deterministic — no shared state or randomness.
- Test behaviour and outputs, not internal implementation details.
- Give tests descriptive names that state the expected behaviour.
- Cover edge cases and error paths, not just the happy path.
- Run tests automatically in CI so they guard every change.
8Key Takeaways
Your first unit test is simpler than it sounds.
- A unit test checks one function's output for a given input.
- Structure every test as Arrange, Act, Assert.
- A runner like Jest or Vitest provides test() and expect().
- Match carefully: toBe for primitives, toEqual for objects.
- Cover edge cases and test behaviour, not implementation.
9Frequently Asked Questions
Q: What is a unit test? A: A unit test is a small automated check that verifies a single piece of code, usually one function, returns the expected result for a given input. It runs in isolation and fails with a precise message when the behaviour is wrong.
Q: What is the Arrange-Act-Assert pattern? A: It is a three-step structure for writing clear tests: Arrange sets up inputs and state, Act calls the code under test, and Assert checks the result against what you expect. Following it consistently makes tests easy to read and debug.
Q: What is the difference between toBe and toEqual? A: toBe checks strict equality and reference identity, so it suits primitives like numbers and strings. toEqual performs a deep comparison of contents, which is what you need for objects and arrays whose references differ but whose values match.
Q: How many tests should I write for one function? A: Enough to cover the important behaviours and edge cases — typically the happy path plus boundaries like empty input, zero, negatives, and error conditions. Each test should focus on one behaviour so a failure pinpoints exactly what broke.
Related Reading
Get The Print Version
Download a PDF of this article for offline reading.
About the Publisher
SkillVeris Team
Engineering Team
Our engineering writers turn abstract code concepts into hands-on, project-driven learning experiences.
View all postsRelated Posts
Never miss an update
Get the latest tutorials and guides delivered to your inbox.
No spam. Unsubscribe anytime.