Unit Testing

So, what exactly is a unit test?

I created this post as a personal exploration of the concept of a “Unit” in the context of testing. My goal is to gain a deeper understanding of how unit testing works and its significance in software development.

I hope that by sharing my insights, others will also find this information helpful.

100%
graph TD classDef unitTest fill:#ff6b6b,stroke:#333,stroke-width:2px,color:white classDef component fill:#339af0,stroke:#333,stroke-width:2px,color:white classDef mock fill:#51cf66,stroke:#333,stroke-width:2px,color:black classDef dependency fill:#ffd43b,stroke:#333,stroke-width:2px,color:black UT[Unit Test]:::unitTest C1[Component Under Test]:::component M1[Mocked Database]:::mock M2[Mocked API]:::mock M3[Mocked Service]:::mock D1[External Dependencies]:::dependency UT -->|Tests| C1 C1 -.->|Replaced by| M1 C1 -.->|Replaced by| M2 C1 -.->|Replaced by| M3 M1 & M2 & M3 -.->|Isolates from| D1 subgraph Test Boundary UT C1 M1 M2 M3 end subgraph External System D1 end

How unit tests interact with components and their dependencies

What is Unit Testing?

In unit testing, a unit refers to the smallest testable part of an application or software. This is typically a function, method, or procedure in a program. The goal of unit testing is to validate that each unit of the software performs as designed.

Key Characteristics of a Unit in Unit Testing:

  1. Small and Isolated:

    • A unit is usually a single function, method, or class.
    • It is tested in isolation, without dependencies on external systems like databases, APIs, or other services.
  2. Focused:

    • The test focuses on a specific piece of functionality or logic in the unit.
    • For example, testing a function that adds two numbers should verify only the addition logic, not its integration with other components.
  3. Deterministic:

    • A unit test should produce the same result every time it is run, given the same input.

Example in Practice:

Code to Test:

function add(a, b) {
    return a + b;
}

Unit Test:

const assert = require('assert');

// Test case for the add function
assert.strictEqual(add(2, 3), 5);  // Test passes
assert.strictEqual(add(-1, 1), 0); // Test passes
100%
graph LR classDef test fill:#ff6b6b,stroke:#333,stroke-width:2px,color:white classDef function fill:#339af0,stroke:#333,stroke-width:2px,color:white classDef input fill:#51cf66,stroke:#333,stroke-width:2px,color:black classDef output fill:#ffd43b,stroke:#333,stroke-width:2px,color:black I[Input]:::input -- Calls --> F[add]:::function F --> O[Returns: 5]:::output T[Unit Test]:::test -->|Tests| F T -->|Verifies| O style T fontSize:18px style F fontSize:18px style I fontSize:16px style O fontSize:16px

A simple unit test verifying an addition function

A unit can be:

To clarify the concept, here’s a UML diagram illustrating a simple unit testing cycle:

Crafting Effective Unit Tests

100%
graph LR classDef red fill:#ff6b6b,stroke:#333,stroke-width:2px,color:white classDef green fill:#51cf66,stroke:#333,stroke-width:2px,color:white classDef blue fill:#339af0,stroke:#333,stroke-width:2px,color:white classDef yellow fill:#ffd43b,stroke:#333,stroke-width:2px,color:white A[Write Test]:::yellow --> B[Red Test Fails]:::red B --> C[Green Code Passes]:::green C --> D[Refactor Improve]:::blue D --> A

The Continuous Cycle of Test-Driven Development (TDD)

1. Structure Your Tests Wisely

When writing tests, consider organizing them like you would a well-structured automation suite:

// A well-structured unit test example
describe('User Authentication Module', () => {
  describe('Password Validation', () => {
    test('should reject a password shorter than 8 characters', () => {
      // Arrange
      const passwordValidator = new PasswordValidator();
      const shortPassword = '123';
      
      // Act
      const result = passwordValidator.validate(shortPassword);
      
      // Assert
      expect(result).toBe(false);
      expect(result.errors).toContain('Password must be at least 8 characters');
    });
  });
});
100%
graph TD A[Arrange] --> B[Act] B --> C[Assert] A --> D[Password Validator Instance] D --> E[Short Password] C --> F[Expected Results]

The structure of a password validation unit test

Tip: Keep your tests organized and related tests grouped together. This makes it easier to maintain and understand your testing code.

2. Mocking Can Be Your Best Friend

Mocking is a powerful technique that many of us know from integration tests, but it’s just as useful for unit tests.

Here’s how to apply it:

test('should handle API errors gracefully', async () => {
  // Arrange
  const mockApiClient = {
    fetchUserData: jest.fn().mockRejectedValue(new Error('Network error'))
  };
  const userService = new UserService(mockApiClient);
  
  // Act
  const result = await userService.getUserProfile(123);
  
  // Assert
  expect(result.success).toBe(false);
  expect(result.error).toContain('Failed to fetch user data');
});
100%
graph TD A[Unit Test] --> B[Mock API Client] B --> C[fetchUserData Method] C --> D[Simulated Error] A --> E[UserService Instance] E --> F[Handle API Response] F --> G[Return Result]

Illustration of mocking an API call in unit tests

3. Don’t Forget Edge Cases

Real-world software often grapples with unusual inputs. Make sure your tests reflect that:

Edge Case Testing for Input Validation

100%
graph TD A[Input Validation Tests] --> B[Input Cases] B --> C[Empty String] B --> D[Whitespace Only] B --> E[Null Value] B --> F[Undefined Value] B --> G[XSS Attempt] C --> H[Sanitize Method] H --> I[Safe Result]

Flow of testing edge cases in input validation

describe('Input Validation', () => {
  const testCases = [
    ['', 'empty string'],
    [' ', 'whitespace only'],
    ['null', 'null value'],
    ['undefined', 'undefined value'],
    ['<script>alert("xss")</script>', 'potential XSS']
  ];

  test.each(testCases)(
    'should sanitize %s (%s)',
    (input, description) => {
      const result = InputSanitizer.sanitize(input);
      expect(result).toBeSafe();
    }
  );
});

Integrating Unit Tests in Your CI/CD Pipeline

Incorporating unit tests into your CI/CD pipeline ensures that each code commit is validated, significantly reducing the risk of bugs reaching production.

For instance, teams have reported up to a 30% decrease in post-release defects after implementing automated testing strategies.

# Example GitHub Actions workflow
name: Test Automation
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Run Unit Tests
        run: |
          npm install
          npm run test:unit
          
      - name: Generate Test Report
        if: always()
        run: npm run generate-test-report
100%
graph TD A[Push Code] --> B[Run Tests] B -->|Success| C[Deployment] B -->|Failure| D[Alert Dev Team]

Integrating unit tests into CI/CD pipeline

Incorporating unit tests into your CI/CD pipeline validates each code commit and significantly reduces the risk of bugs in production.

Good vs. Poor Unit Tests Illustrated

Let’s look at the difference between poorly structured and well-structured unit tests:

// 🚫 Poor Unit Test Example
[Test]
public void TestUserRegistration()
{
    var user = new User();
    user.Email = "test@example.com";
    user.Password = "password123";
    var result = user.Register();
    
    Assert.IsTrue(result.Success);
    Assert.IsTrue(_database.Contains(user));  // Violating isolation
    Assert.IsTrue(_emailService.EmailSent);   // Testing multiple concerns
}

// ✅ Excellent Unit Test Example
[Test]
public void ValidatePassword_WithWeakPassword_ReturnsFalse()
{
    // Arrange
    var passwordValidator = new PasswordValidator();
    var weakPassword = "123";
    
    // Act
    bool isValid = passwordValidator.Validate(weakPassword);
    
    // Assert
    Assert.IsFalse(isValid, "Weak password should be rejected");
}

The poor test’s reliance on the database and external services introduces coupling, making it susceptible to failure regardless of the unit’s functionality. In contrast, the excellent unit test focuses solely on the password validation logic, ensuring that it is isolated and reliable.

Who Writes Unit Tests? QAs or Devs?

The responsibility of writing unit tests typically falls on developers. Since they know the code best, developers are in the best position to ensure that individual pieces of code work as expected. Unit tests are often written alongside the code, especially in practices like test-driven development (TDD).

QA engineers focus more on higher-level testing like integration and end-to-end tests, but they may also contribute to unit tests in smaller teams, particularly in test automation and coverage maintenance.

While developers mainly write unit tests, collaboration with QA engineers ensures comprehensive software testing.

Conclusion

Unit testing isn’t just a chore; it’s an empowering practice that enhances confidence in your code.

It enables faster, more reliable development, making it a strategic asset in your toolkit.

Good Luck. :P

Additional Resources

To delve deeper into unit testing, check out these resources:

Disclaimer: This post is for personal use, but I hope it can also help others. I'm sharing my thoughts and experiences here.
If you have any insights or feedback, please reach out!
Note: Some content on this site may have been formatted using AI.

Stay Updated

Subscribe my newsletter

© 2025 Pavlin

Instagram GitHub