Testing

You have written the code. Features are implemented. The task management app has endpoints, services, and a database schema. Now comes the phase that separates professional software from hobby projects: testing.

This is post #9 in the How It Works series. We are not doing a deep dive into testing techniques — there is a separate Test Coverage post in the Best Practices series for that. This post is about testing as a phase in the development lifecycle.

1. Why Testing is a Phase, Not an Afterthought

Testing is not just writing unit tests and calling it a day. It is a deliberate phase where you verify the entire system works as designed. You are answering one question: does this software do what we said it would do?

During implementation, you focus on making things work. During testing, you focus on proving they work — and finding where they do not. Implementation is creative. Testing is adversarial. You are trying to break your own work.

When teams skip a dedicated testing phase, they ship bugs to production. Every time. In our task management app, we built features like creating tasks, assigning them, and marking them complete. The testing phase is where we verify all of that actually works — individually, together, and under real-world conditions.

2. The Test Pyramid

Not all tests are created equal. The test pyramid gives you a framework for how many of each type to write:

  • Unit Tests (base) — Many of these. Fast, isolated, testing individual methods. You should have hundreds or thousands.
  • Integration Tests (middle) — Fewer. They test how components work together — API to service to database. Slower because they involve real infrastructure.
  • End-to-End Tests (top) — Fewest. They test complete user workflows. Slowest and most brittle, but catch issues lower-level tests miss.

The shape matters. Invert it — lots of E2E tests, few unit tests — and your suite will be slow, fragile, and painful to maintain.

3. Unit Testing Our Task App

Unit tests verify business logic in isolation. No database, no network. For our task app, when a task is created it should have a TODO status, a timestamp, and the provided title. Let us test that.

Java — JUnit 5

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import static org.junit.jupiter.api.Assertions.*;
import java.time.LocalDateTime;

class TaskServiceTest {

    private TaskService taskService;

    @BeforeEach
    void setUp() {
        TaskRepository mockRepo = new InMemoryTaskRepository();
        taskService = new TaskService(mockRepo);
    }

    @Test
    void createTask_shouldSetDefaultFields() {
        TaskRequest request = new TaskRequest("Build login page", "Implement OAuth2 login flow");

        Task task = taskService.createTask(request);

        assertNotNull(task.getId());
        assertEquals("Build login page", task.getTitle());
        assertEquals(TaskStatus.TODO, task.getStatus());
        assertNotNull(task.getCreatedAt());
        assertTrue(task.getCreatedAt().isBefore(LocalDateTime.now().plusSeconds(1)));
    }

    @Test
    void createTask_shouldRejectEmptyTitle() {
        TaskRequest request = new TaskRequest("", "Some description");

        assertThrows(IllegalArgumentException.class, () -> {
            taskService.createTask(request);
        });
    }
}

This test runs in milliseconds. It verifies that creating a task sets the right defaults, and that the service rejects invalid input.

Python — pytest

import pytest
from datetime import datetime
from task_service import TaskService
from task_repository import InMemoryTaskRepository

@pytest.fixture
def task_service():
    repo = InMemoryTaskRepository()
    return TaskService(repo)

def test_create_task_sets_default_fields(task_service):
    task = task_service.create_task(
        title="Build login page",
        description="Implement OAuth2 login flow"
    )

    assert task.id is not None
    assert task.title == "Build login page"
    assert task.status == "TODO"
    assert isinstance(task.created_at, datetime)

def test_create_task_rejects_empty_title(task_service):
    with pytest.raises(ValueError, match="Title cannot be empty"):
        task_service.create_task(title="", description="Some description")

Same logic, different language. Set up a service with a fake repository, call the method, verify the result.

4. Integration Testing

Unit tests prove your logic is correct. Integration tests prove your components work together — real HTTP requests, real database queries, real serialization.

For our task app, an integration test starts the application, sends an HTTP request to create a task, and verifies it is stored correctly. In Spring Boot you use @SpringBootTest with TestRestTemplate. In Flask you use the test client.

These tests are slower but catch issues unit tests cannot see: wrong JSON field names, database constraint violations, incorrect HTTP status codes, and misconfigured authentication.

5. End-to-End Testing

E2E tests simulate a real user. For our task app: user logs in, creates a task, assigns it to a teammate, the teammate marks it complete, and we verify the status updates correctly.

These run against the full deployed application — frontend, backend, database, everything. The tools have gotten very good:

  • Playwright — Modern, fast, supports multiple browsers. The best choice for most teams today.
  • Cypress — Developer-friendly with excellent debugging. Strong in the JavaScript ecosystem.
  • Selenium — The original browser automation tool. Mature but more verbose.

E2E tests are expensive to maintain and can be flaky. Write them for your critical user paths and rely on unit and integration tests for everything else.

6. Manual Testing and QA

Automated tests are essential, but they cannot catch everything.

Exploratory testing means using the app without a script — resizing windows, pasting special characters, clicking rapidly, hitting the back button. Testers do things automated scripts would never think to do.

Edge case testing targets boundaries. What happens with a 10,000-character title? What if two users edit the same task simultaneously?

Usability testing asks not “does it work?” but “is it confusing?” A feature can be technically correct but poorly designed. On professional teams, a human being should use the software before it ships.

7. Senior Tip

“If it is not tested, it is broken.”

Untested code has bugs — you just have not found them yet. The best developers write tests as they build. You write a function, you write a test. You add an endpoint, you add an integration test. Testing is part of implementation, not separate from it.

The testing “phase” is about going beyond what you tested while building — running the full suite, doing E2E testing, and bringing in QA. But the foundation should already exist by the time you reach this phase. Build the habit early.




Subscribe To Our Newsletter
You will receive our latest post and tutorial.
Thank you for subscribing!

required
required


Leave a Reply

Your email address will not be published. Required fields are marked *