TL;DR

Understanding software testing from multiple perspectives

Hi, I'm Gunner (Jung Jae-hoon), a backend engineer at ONDA building hotel operations software. After years developing backend systems across different domains, I've noticed two things: testing is a huge topic, and people argue endlessly about whether it's actually necessary.

So I'm splitting this into two parts — first, the types of tests. Second, what developers should actually test.

1. Types of Software Testing

Software testing is the process of verifying that software works correctly and finding bugs before users do.

Testing breaks down into two big buckets: Black-Box and White-Box.

  • Black-Box (Functional) Testing: Treat the software like a sealed box. You can't see inside. You test from the user's perspective — inputs, outputs, features. Source code isn't the concern. QA teams typically own this.

  • White-Box (Structural) Testing: Open the box. Inspect the code, program structure, data flows. Verify logic at the implementation level. Developers own this.

You can also classify tests by the level they operate at:

  • Unit Testing: Test the smallest building block in isolation. Separate one piece from the rest of the system, run it independently, check the result against expectations. What counts as a "unit" depends on your language or framework — in functional programming, it's a function. In OOP, it's usually a class.

  • Integration Testing: Systems are made of components talking to each other. Integration tests verify those interactions — within the same system or across systems.

    • Regression Testing: After you add a feature or fix a bug, does the existing system still work? That's what regression tests check.

    *Integration tests can be regression tests, but not always. They're related but not synonyms.

  • System Testing: Test the complete, integrated system against its requirements. Unlike black-box testing, you may need to peek inside (e.g., spin up a browser, run code-level checks). Unlike integration testing, you're treating the whole thing as one system. You test functional requirements and non-functional ones (stability, scalability, etc.).

2. Testing Object-Oriented Systems

Most modern software is object-oriented. OOP emerged in the 1960s with Simula (a simulation language) and now dominates software development.

In object-oriented systems, what's the "unit" in unit testing?

The fundamental building block of an OO system is the object. But objects are described by classes — so the unit is typically the class.

A class has fields, methods, inheritance, and all that gets tangled together. When you write unit tests for a class, you usually test its methods. That might make you think the method is the unit. But methods can't be separated from their class — so the class is still the right boundary.

When you treat the class as the unit, unit tests come in two flavors:

  • Intra-class tests: Test only within the target class.
  • Inter-class tests: The target class interacts with other classes during the test.

Wait — what's the difference between inter-class unit tests and integration tests?

In OO systems, one test case often involves multiple classes. So the line between unit and integration tests gets blurry. Integration tests are about "testing interactions between components" — but what's a "component"?

If you define a component as one class, then the distinction collapses. There's no universal answer. But here's a practical heuristic:

  • Unit tests emphasize isolation and speed. Use mocks to decouple from other parts of the system. Run fast.
  • Integration tests use real code, no mocking. They involve more classes and more complex interactions.

Still fuzzy? Yeah. Welcome to OO testing.

3. BDD and TDD

This fuzziness shows up in testing methodologies too. Test-Driven Development (TDD) focuses on unit tests. Behavior-Driven Development (BDD), which evolved from TDD, shifts focus to system behavior — making behavior explicit and testable.

BDD uses the "Given/When/Then" structure:

  • Given: Set up the initial state before the behavior happens.
  • When: Trigger the event (the behavior being tested).
  • Then: Check the expected outcome.

Here's an example BDD test in Scala:

package com.acme.pizza

import org.scalatest.FunSpec
import org.scalatest.BeforeAndAfter
import org.scalatest.GivenWhenThen

class PizzaSpec extends FunSpec with GivenWhenThen {

  var pizza: Pizza = _

  describe("Pizza") {

    it ("should allow adding toppings") {
      Given("a new pizza")
      pizza = new Pizza

      When("a topping is added")
      pizza.addTopping(Topping("green olives"))

      Then("the topping count increases")
      expectResult(1) {
        pizza.getToppings.size
      }

      And("the topping type is preserved")
      val t = pizza.getToppings(0)
      assert(t === new Topping("green olives"))
    }
  }
}

Why does BDD emphasize "Given/When/Then"?

Object-oriented systems work through object interactions. Unit tests alone often aren't enough. When writing test cases, you need to focus on those interactions.

If you use a unit testing framework like JUnit, there's an implicit convention: one test file per class. That nudges you toward intra-class thinking. But if you start with "Given/When/Then," you naturally think about interactions across classes.

Using BDD encourages inter-class thinking. That leads to better tests.

Of course, following "Given/When/Then" doesn't automatically make your tests interaction-focused. The structure works for unit tests and integration tests alike. What matters most is how the test author thinks about the scope.

So what is TDD, and what do people argue about?

References

1. Behavior-driven development - Wikipedia