1. What is Unit Testing?

  • Unit testing involves testing individual units or components of a software application in isolation.
  • A “unit” could be a function, method, or module that performs a specific task.
  • Unit tests are used to validate that each part of the software behaves as expected under various scenarios, such as typical inputs, edge cases, or invalid inputs.

2. Purpose of Adding Unit Tests to CI Pipelines:

  • Early Detection: Helps identify and fix bugs early in the development cycle.
  • Automation: Automated testing ensures consistency and saves time.
  • Code Quality: Encourages good coding practices by ensuring new code doesn’t introduce regressions.
  • Confidence in Changes: Provides confidence that code changes will not break existing functionality.

3. Steps to Implement Unit Testing:

  1. Identify the Functionality to Test:

    • Choose functions, modules, or components that perform a critical task.
    • Ensure the tests cover typical cases, edge cases, and potential failure scenarios.
  2. Create a Test File or Module:

    • Follow naming conventions that are recognized by your testing framework (e.g., file names ending in _test, or using specific folders like tests/).
    • Use the standard testing framework of your language or third-party libraries as needed.
  3. Write the Tests:

    • Structure each test function to check a specific aspect of the code.
    • Use assertions to validate expected outcomes.
    • For example:
      • Positive Cases: Ensure correct output for valid inputs.
      • Negative Cases: Check how the code handles invalid or missing inputs.
  4. Run Tests Locally:

    • Before pushing changes, run all tests in your local environment to ensure they pass.
    • Example commands:
      • For Python: pytest
      • For JavaScript: npm test
      • For Java: mvn test

4. Integrating Unit Tests into a CI/CD Pipeline:

  • Modify your CI configuration file (e.g., .yml, .json, etc.) to include a step for running tests.

  • Example of a CI Configuration:

    name: CI Pipeline
     
    on:
      pull_request:
        branches: [main]
     
    jobs:
      run-tests:
        name: Run Unit Tests
        runs-on: ubuntu-latest
     
        steps:
          - name: Check out code
            uses: actions/checkout@v4
     
          - name: Set up Environment
            run: |
              # Commands to set up language runtime, dependencies, etc.
              # Example: For Python: python -m venv venv && venv/bin/pip install -r requirements.txt
              # Example: For JavaScript: npm install
     
          - name: Run Unit Tests
            run: |
              # Run the test command for your language
              # Example: For Python: venv/bin/python -m pytest
              # Example: For JavaScript: npm test
  • Explanation:

    • Trigger: The CI pipeline is triggered on a pull request to the main branch.
    • Job Definition: The pipeline has a single job called run-tests.
    • Steps:
      • Check out the repository code.
      • Set up the runtime environment (e.g., install dependencies).
        • Each run: step is a new shell, so source venv/bin/activate doesn’t carry over to the next step. Call venv/bin/python directly (or activate and test in the same step), or use actions/setup-python.
      • Run unit tests using the appropriate command for the language or framework.

5. Handling Test Results:

  • If any test fails, the CI job will fail, and the developer will be notified.
  • Review the logs for detailed error messages and fix the failing tests.
  • Once the issues are resolved, push the changes, and the tests will re-run automatically.

6. Common Best Practices for Unit Testing:

  • Isolate Tests: Each test should be independent, without relying on external systems (e.g., databases, APIs) unless necessary.
  • Use Mocks/Stubs: Use mock data or stubs to simulate dependencies.
  • Write Descriptive Test Names: Clearly indicate what each test is checking.
  • Aim for High Test Coverage: Ensure that a significant portion of the code is covered by tests.

Key Takeaways

  • Implementing unit tests in a CI pipeline helps catch bugs early and automates the testing process.
  • Write tests that cover a range of scenarios, ensuring code quality and reliability.
  • Use CI/CD tools to automatically run these tests for each pull request, creating a robust development workflow.

Code Coverage

Code coverage is a measure of how much of your code is being tested. It’s a controversial metric, but I’ll try to provide a balanced take on… at least as balanced as I can be with my personal biases.

For example, if you have 1000 lines of code in your project, and you have tests that cover the logic in 500 of those lines, then we’d say that you have 50% code coverage.

Why is code coverage controversial?

One reason is that it’s quite possible to have 100% code coverage and still have bugs in your code. It’s also possible to have 0% code coverage and have a bug-free application. Unit tests help us find bugs and codify the expected behavior of individual units of code, but they don’t guarantee that there are no bugs.

My personal take is that it’s really hard to say “20% is bad, 80% is good”. I think some functions and methods are more important to have unit tests for than others. For example, I don’t love the idea of unit testing external systems, like databases.

I think that’s a better use case for integration tests.

What does this mean to you?

As a junior developer, you should know what code coverage is, and you should be amicable to the code coverage requirements of the company you work for. You’ll certainly develop your own opinions (and should politely vocalize them) about its efficacy over time, but it’s important to be a team player and not rock the boat too much when you’re just starting out and lack your own experience.

README Badge for Tests

One cool feature on GitHub is that you can add a dynamic badge to your README.md file that shows the status of your tests.

It’s a great way to show off that your code is well-tested, and that the tests are passing without users having to go check the Actions tab on GitHub.

Assignment

Add a badge to the top of your README.md file that shows the status of your tests.

Dynamic Badge URL

The syntax for the URL of the dynamically generated image is:

<https://github.com/><OWNER>/<REPOSITORY>/actions/workflows/<WORKFLOW_FILE>/badge.svg

Markdown Syntax

Your README.md file is written in Markdown, and the syntax for adding an image is:


![ALT_TEXT](IMAGE_URL)

For example:


![code coverage badge](https://github.com/wagslane/go-rabbitmq/actions/workflows/Tests.yml/badge.svg)

Steps to Implement

  1. Go to the .github/workflows directory and find the CI workflow file (e.g., ci.yml).
  2. Replace <OWNER> with your GitHub username or organization name.
  3. Replace <REPOSITORY> with your repository name.
  4. Replace <WORKFLOW_FILE> with the exact name of your workflow file (e.g., ci.yml).
  5. Add the generated badge URL at the top of your README.md file using the Markdown syntax.
  6. Commit and push your changes to GitHub.

Once your changes are merged into the main branch, you should see the badge appear on the main page of your repository in the rendered README.

Example

If your GitHub repository is named learn-cicd, and your GitHub username is johnDoe, and the workflow file is named ci.yml, the badge syntax would be:


![test status](https://github.com/johnDoe/learn-cicd/actions/workflows/ci.yml/badge.svg)

Why Use Badges?

  • Visibility: The badge makes the status of your tests visible to anyone viewing the repository, enhancing credibility.
  • Quick Insights: Users can quickly see if your project is well-maintained and if the tests are currently passing.
  • Showcase: It showcases good CI/CD practices, which is helpful for open-source contributors and potential collaborators.