DEV Community

Muhammad Refel Hidayatullah
Muhammad Refel Hidayatullah

Posted on • Edited on

Summary of TDD that I tried to learn

1. The Cycle of TDD

- Write a Test First

Before writing implementation code, write a test that specifies how the code should behave. This test will typically fail at first because the code hasn't been implemented yet.

- Write Just Enough Code

Write only the code necessary to make the test pass. Do not add features beyond the scope of the test.

- Refactor

Once the test passes, refactor the code to improve its quality without changing its functionality.

2. TDD Mindset

- Start with Failure (Fail First):

Get used to writing tests for features that do not yet exist. This requires discipline and the mindset that "failure is normal" at the beginning of the process.

- Small, Focused, and Iterative

Avoid trying to implement large features all at once. TDD encourages breaking work into small parts and focusing on one thing at a time.

- Design by Tests

TDD is not just about writing tests; it's also about design. Writing tests before code forces you to think about the best way to design APIs or functions to make them easy to test.

3. Testing Approaches in TDD

- Unit Test

Focus on the smallest unit of code, typically a function or class.

  1. Ensure tests are deterministic (the same result every time they run).
  2. Use mocking or stubbing to isolate external dependencies.

- Integration Test

Test interactions between multiple units.

  1. Ensure modules work well together.
  2. Focus on data flow between components.

- End-to-End (E2E) Test

Test the entire system from the user's perspective.

  1. Use this only for key features, as these tests are time-consuming.

4. Strategies to Avoid TDD Failures

Write Accurate Tests

Don't just test the "happy path." Write tests for edge cases or error scenarios:

  • Zero values (0)
  • Empty strings
  • Invalid inputs (e.g., null or undefined)

- Use Coverage Reports

Ensure your code is thoroughly tested. Use tools like Istanbul, Jest, or SonarQube to check test coverage.

- Manage Dependencies

Avoid tight coupling between modules. Use techniques like:

  • Mocking to replace dependencies with fake versions.
  • Inversion of Control (IoC) or dependency injection to make components easier to test.

5. Organizing Tests

a. Clear Test Structure

Use the Arrange-Act-Assert (AAA) format:

  • Arrange: Set up all data, dependencies, or initial conditions.
  • Act: Call the function or perform the action you want to test.
  • Assert: Verify that the result matches the expected outcome.

b. Use Descriptive Naming

Test names should be clear and explain what is being tested.

c. Organize Test Folders

Keep tests in a well-organized folder structure.

6. Design Principles for TDD

a. Single Responsibility Principle (SRP)

Ensure functions or classes have only one responsibility, making them easier to test.

b. Open/Closed Principle

Design components to be extensible without modifying existing code.

c. Dependency Injection

Avoid creating direct dependencies inside a class. Instead, inject dependencies from the outside.

7. Advanced Concepts in TDD

a. Test Doubles

  • Mock: Replace real objects with fake ones to control behavior and verify interactions.
  • Stub: Provide fake responses for a function.
  • Spy: Track how a function is called.

b. Behavior-Driven Development (BDD)

An advanced form of TDD, where tests are written in a human-readable language, often using frameworks like Cucumber.

Scenario: Adding two numbers
Given I have numbers 2 and 3
When I sum the numbers
Then the result should be 5
Enter fullscreen mode Exit fullscreen mode

8. Advanced Tips

1. Outside-In Development (London School TDD)

Approach: Start with high-level tests like integration or acceptance tests before moving to unit testing.

Main Principle: Focus on behavior rather than implementation. Develop the system based on how components should interact.

Example:

Start with testing an API endpoint (e.g., REST or GraphQL), then move to the service, repository, and class-level implementation.

Benefits:

  • Avoid over-engineering by ensuring only necessary code is written.
  • Focus on functional outcomes.

2. Mutation Testing

Concept: Evaluate test quality by introducing small changes (mutations) in the source code and checking if tests catch those changes.

If tests don't fail after a mutation, it indicates a lack of coverage or precision.

Tools:

  • JavaScript: Stryker
  • Java: Pitest

Benefits:

  • Measure how robust your tests are against code changes.
  • Identify high-risk areas for bugs.

3. Mocking and Stubbing Best Practices

When testing components that depend on external services or APIs, use mocking and stubbing.

General Principles:

  • Use mocks to verify interactions (e.g., whether a method was called).
  • Use stubs to provide fixed responses (dummy data) from dependencies.

4. Property-Based Testing

Concept: Instead of testing specific cases, define general properties or rules that must always hold true.

Example: "The result of merging two arrays must always equal the combined length of both arrays."

Tools:

  • JavaScript: Fast-check

Benefits:

  • Discover hidden bugs by testing many input combinations.
  • Ideal for complex algorithms like sorting or encryption.

5. Golden Master Testing

Concept: Used for legacy systems or complex code without prior documentation or tests.

Take a snapshot of the current behavior or output (golden master) and use it as a baseline for future tests.

Benefits:

  • Enables refactoring of old systems without breaking functionality.
  • Highly effective for large or monolithic systems.

6. Contract Testing

Concept: Ensure that the contract between two services, such as an API and its consumer, is not broken.

Tools:

  • Pact (for API contract testing).

Benefits:

  • Increases confidence when making changes or iterating on dependent services.
  • Reduces integration risks.

7. Exploratory Testing with TDD

Approach: Combine TDD principles with manual exploration for new features or experimental algorithms.

Method:

  • Write initial tests to "lock in" known behavior.
  • Use additional tests to explore how the code reacts to new inputs or scenarios.

Benefits:

  • Uncover edge cases that might have been missed.
  • Accelerate validation of new functionality.

8. Delayed Implementation

For cases where a feature or function cannot yet be implemented (e.g., due to unfinished dependencies), use a test placeholder.

describe('New Feature', () => {
  it('should handle specific edge case (TODO: implement)', () => {
    // Tes sementara
    expect(true).toBe(true); 
  });
});
Enter fullscreen mode Exit fullscreen mode

Benefits:

  • Reminds the team to complete pending parts.
  • Breaks work into manageable steps.

9. Working with Legacy Code

If legacy code lacks tests:

  • Add Characterization Tests to "lock in" current behavior.
  • Write tests for small parts of the code you plan to modify.
  • Gradually refactor while ensuring tests pass.

Benefits:

  • Introduces TDD into systems that were not initially designed for it.
  • Reduces refactoring risks in legacy systems.

Sentry blog image

How to reduce TTFB

In the past few years in the web dev world, we’ve seen a significant push towards rendering our websites on the server. Doing so is better for SEO and performs better on low-powered devices, but one thing we had to sacrifice is TTFB.

In this article, we’ll see how we can identify what makes our TTFB high so we can fix it.

Read more

Top comments (1)

Collapse
 
raisulkhairi profile image
raisul khairi • Edited

Awesome! ,for the next time, please make an article to test the computed, signal and effect

AWS Security LIVE!

Join us for AWS Security LIVE!

Discover the future of cloud security. Tune in live for trends, tips, and solutions from AWS and AWS Partners.

Learn More

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay