CICD

CICD

Continuous Integration (CI) and GitHub Actions

  1. What is Continuous Integration (CI)?

    • CI is the practice where developers regularly integrate their code into a shared repository.
    • Each time code is pushed, automated builds and tests are run to ensure that new changes do not break the existing codebase.
    • The types of tests that can be included in CI are:
      • Unit Tests
      • Integration Tests
      • Styling Checks
      • Linting Checks
      • Security Checks
    • If any test fails, the build is marked as broken, and the developer is notified.
  2. Advantages of CI:

    • Automates the testing and review process.
    • Reduces the need for manual checks, such as code formatting or running tests locally.
    • Helps maintain a healthy and stable codebase by catching issues early.
  3. Internal CI at Boot.dev:

    • Example: Boot.dev runs CI checks every time a new pull request is opened.
    • The CI process includes:
      • Unit Tests: Runs Go tests with the command go test.
      • Formatting Checks: Ensures the code is formatted correctly using go fmt.
    • This automation streamlines part of the code review process, focusing on logical and architectural reviews.
  4. GitHub Actions for CI:

    • GitHub Actions is a CI/CD tool provided by GitHub that uses YAML files to define workflows.
    • A typical CI setup involves creating a .github/workflows directory in the repository.
    • Inside this directory, a workflow file (e.g., ci.yml) is added to define what actions should be performed when certain events occur (e.g., opening a pull request).
  5. Creating a Basic CI Workflow (ci.yml):

    • Directory Structure:

      mkdir .github
      mkdir .github/workflows
      touch .github/workflows/ci.yml
    • Workflow File (ci.yml):

      name: ci
       
      on:
        pull_request:
          branches: [main]
       
      jobs:
        tests:
          name: Tests
          runs-on: ubuntu-latest
       
          steps:
            - name: Check out code
              uses: actions/checkout@v4
       
            - name: Set up Go
              uses: actions/setup-go@v5
              with:
                go-version: '1.23.0'
       
            - name: Force Failure
              run: (exit 1)
    • Explanation:

      • name: Names the workflow as ci.
      • on: Specifies the event that triggers the workflow (pull_request to the main branch).
      • jobs: Defines a set of tasks (jobs) that will be run.
      • runs-on: Specifies the environment (ubuntu-latest) to run the job on.
      • steps: Defines the sequence of commands to execute.
        • Check out code: Retrieves the code from the repository using actions/checkout@v4.
        • Set up Go: Installs the specified Go version using actions/setup-go@v5.
        • Force Failure: Runs exit 1, which will cause the step and workflow to fail (for testing purposes).
  6. Success and Failure in GitHub Actions:

    • A step “succeeds” if it exits with a status code of 0.
    • A step “fails” if it exits with a status code other than 0 (typically 1).
    • The Force Failure step (run: (exit 1)) is intentionally designed to fail, demonstrating how CI handles test failures.
  7. Testing the CI Workflow:

    • After creating the workflow file, push the changes to a remote branch.
    • Open a pull request to the main branch.
    • GitHub Actions will automatically run the defined ci workflow.
    • Because of the Force Failure step, the workflow will fail, confirming that the CI setup is correctly configured.
  8. Conclusion:

    • This basic CI setup is a starting point for building more complex workflows.
    • Additional tests, linters, and code quality checks can be added as more steps in the steps section.
    • CI is essential for maintaining a robust development process by automating repetitive tasks and providing quick feedback to developers.

Breakdown of GitHub Actions Concepts

  1. Workflows:

    • A workflow is a series of automated tasks defined using a .yml or .yaml file located in the .github/workflows directory.

    • Workflows are triggered by events such as a push, pull_request, or any other event that occurs in the GitHub repository.

    • In the example workflow file:

      name: CI
      on:
        pull_request:
          branches: [main]
      • name: Specifies the name of the workflow (CI in this case).
      • on: Defines the event that triggers the workflow.
        • pull_request: This workflow is triggered whenever a pull request targeting the main branch is opened, updated (new commits = synchronize), or reopened. A merge is a closed event (with merged == true), which isn’t in the defaults, so to run something on merge use on: push to main.
      • Workflows can have any name (e.g., ci, build, test), which helps identify the workflow’s purpose.
  2. Jobs:

    • A workflow is made up of one or more jobs.

    • A job is a set of steps that run on the same runner (a virtual machine instance provided by GitHub to execute the job).

    • Jobs are defined under the jobs section in the YAML file.

    • Each job can be run on a different environment (e.g., Linux, Windows, macOS).

    • Example of a single job called Tests:

      jobs:
        tests:
          name: Tests
          runs-on: ubuntu-latest
      • name: Specifies the name of the job (Tests in this case).
      • runs-on: Specifies the operating system for the job (ubuntu-latest in this case).
    • Multiple jobs can be added to a single workflow to:

      • Run in parallel (e.g., testing different components simultaneously).
      • Test on multiple platforms (e.g., Windows, Linux, macOS).
  3. Steps:

    • A job is made up of a series of steps, where each step performs a specific task.

    • Steps can:

      • Run commands.
      • Execute scripts.
      • Use actions (reusable components).
    • Example of steps in a job:

      steps:
        - name: Check out code
          uses: actions/checkout@v4
       
        - name: Set up Go
          uses: actions/setup-go@v5
          with:
            go-version: '1.23.0'
       
        - name: Force Failure
          run: (exit 1)
      • name: A human-readable name for the step.
      • uses: Refers to a prebuilt GitHub Action to perform a task.
        • actions/checkout@v4: Checks out the repository’s code to the runner.
        • actions/setup-go@v5: Sets up the Go environment on the runner.
      • run: Executes a shell command or script.
        • run: (exit 1): Runs a command that exits with a status code of 1, causing the step to fail intentionally.
  4. Detailed Breakdown of the Workflow Example:

    name: ci
     
    on:
      pull_request:
        branches: [main]
     
    jobs:
      tests:
        name: Tests
        runs-on: ubuntu-latest
     
        steps:
          - name: Check out code
            uses: actions/checkout@v4
     
          - name: Set up Go
            uses: actions/setup-go@v5
            with:
              go-version: '1.23.0'
     
          - name: Force Failure
            run: (exit 1)
    • Workflow Name:

      • The workflow is named ci.
      • It is triggered whenever a pull request is opened or updated in the main branch.
    • Job: Tests

      • The workflow contains a single job named Tests.
      • This job will run on ubuntu-latest, which is the latest Ubuntu distribution available on GitHub’s servers.
    • Steps in the Tests Job:

      1. Check out code:
        • Uses the actions/checkout@v4 action to check out the source code from the repository.
      2. Set up Go:
        • Uses the actions/setup-go@v5 action to set up the Go programming environment.
        • Specifies the go-version to be 1.23.0.
      3. Force Failure:
        • Runs the command exit 1, which will always fail the job intentionally.
        • This is useful for testing whether the CI is correctly catching failures and reporting them.
  5. Understanding Job Success and Failure:

    • Success: A step succeeds if it exits with a status code of 0.
    • Failure: A step fails if it exits with a non-zero status code (e.g., 1).
    • In this example, the Force Failure step will always fail because it explicitly exits with a status code of 1.
  6. Expanding the Workflow:

    • This workflow can be expanded by adding more jobs and steps such as:
      • Running additional tests (e.g., unit tests, integration tests).
      • Performing linting or formatting checks.
      • Adding deployment steps to automate deployment after the tests pass.
    • Each new job can have its own set of steps, environment, and conditions for execution.
  7. Conclusion:

    • This basic CI setup with a single job and a few steps is a foundational structure that can be built upon.
    • As development progresses, more comprehensive workflows can be created, including multiple jobs for different test cases, environments, and build configurations.
Link to original

CICD_Build

CICD_Build

Continuous Deployment (CD)

Continuous Deployment (CD) involves automatically deploying code to a production environment after successful testing and building. The objective is to automate the release process, so new changes are immediately deployed to production once merged or pushed to the main branch.

Steps for Setting up the CD Workflow

  1. Create a new workflow file: cd.yml in the .github/workflows directory.
  2. Define the workflow trigger: It should trigger on push events to the main branch.
  3. Create a Deploy job with the following steps:
    • Checkout code.
    • Set up the Go toolchain.
    • Build the application using the scripts/buildprod.sh script.

Sample cd.yml Workflow File

# .github/workflows/cd.yml
name: Continuous Deployment
 
on:
  push:
    branches: [main]
 
jobs:
  Deploy:
    name: Deploy
    runs-on: ubuntu-latest
 
    steps:
      - name: Check out code
        uses: actions/checkout@v4
 
      - name: Set up Go
        uses: actions/setup-go@v5
        with:
          go-version: "1.23.0"
 
      - name: Build the app
        run: scripts/buildprod.sh

Workflow Explanation

  1. Trigger Configuration:

    • The workflow triggers on push events to the main branch.
    • This ensures the deployment workflow runs whenever code is pushed directly to main or merged into main via a pull request.
  2. Deploy Job Setup:

    • Job Name: Deploy.
    • Runs on: ubuntu-latest (the latest Ubuntu runner available in GitHub Actions).
  3. Steps:

    • Check out code: Uses actions/checkout@v4 to clone the repository into the runner.
    • Set up Go: Uses actions/setup-go@v5 to install the specified Go version.
    • Build the app: Executes the custom script scripts/buildprod.sh to build the production version of the application.
    • Note: Despite the job name, this sample only builds. Nothing is pushed or deployed yet. Real CD adds a step that pushes the artifact/image to a registry and a deploy step (using secrets or OIDC credentials).

Running the CD Workflow

  1. Create or update a pull request targeting the main branch.
  2. Once merged into the main branch, the cd.yml workflow should automatically run.
  3. Alternatively, you can push directly to main to trigger the workflow.

Key Differences Between ci.yml and cd.yml

  • ci.yml: Executes on pull requests and ensures the code changes are tested and formatted correctly before merging.
  • cd.yml: Executes on push to main to build the code (and, once push + deploy steps are added, ship it to production).

Troubleshooting Tips

  1. Check the scripts/buildprod.sh Path:

    • Ensure the scripts/buildprod.sh script exists and has executable permissions (chmod +x scripts/buildprod.sh).
  2. Ensure Correct Go Version:

    • If there are Go version issues, verify the Go version specified in the workflow matches your project’s Go version.
  3. Workflow Debugging:

    • If the workflow fails, click on the Actions tab in GitHub, navigate to the cd workflow run, and review the logs for any error messages.

Notes

  • Always review the deployment process and environment configurations to avoid unexpected downtime.
  • Consider adding deployment to a staging environment before pushing directly to production.

This setup should help automate the deployment of your Notely project efficiently. Let me know if you’d like to expand the workflow to include additional deployment steps, such as Docker builds or Kubernetes deployments!

Link to original

CI/CD

CICD_Formatting

CICD_Formatting

CI/CD - Formatting

Automated Code Formatting

Automated code formatting is a great way to keep your code consistent and readable. It helps avoid arguments about code style, a phenomenon often referred to as “bikeshedding”.

Why Use Automated Code Formatting?

  • Consistency: Ensures that code is uniformly styled throughout the project.
  • Readability: Properly formatted code is easier to read and understand.
  • Focus: Allows developers to focus on actual code logic and functionality instead of debating over style issues.

Personal Anecdote

Many developers prefer to enforce automated code formatting from the start of a project. For example, in the Go programming language, a built-in, opinionated code formatter called go fmt is widely used. Most serious Go projects use this formatter to maintain consistency across the codebase.

Example: Unformatted vs. Formatted Code

Here’s an example of technically valid, but poorly formatted Go code:

func main() {
 fmt.Println("hello world!") }

Formatted correctly, it would look like this:

func main() {
    fmt.Println("hello world!")
}

Using go fmt

The go fmt command is a built-in Go tool that automatically formats Go code. Since it is part of the Go toolchain, you don’t need to install anything additional to use it.

Command Syntax

To format a Go file or an entire project, simply run:

go fmt ./...

This command formats all .go files in the current directory and its subdirectories.

VS Code Integration

Many developers configure their code editors to format code automatically on save. For example, in VS Code, you can set up auto-formatting for Go (or any other language) to ensure your code always follows the proper style conventions.

Benefits of Auto-Formatting on Save

  • Saves Time: Developers don’t have to manually run formatting commands.
  • Reduces Errors: Automatically corrects minor formatting mistakes.
  • Prevents Style Debates: Eliminates subjective discussions around code styling.

Application in Projects

In most projects, formatting is enforced as part of the Continuous Integration (CI) pipeline, where code is checked and formatted before merging into the main branch. This ensures that all code meets the project’s styling guidelines.

Incorporating automated formatting tools is a recommended practice, whether you’re working alone or in a team environment, as it promotes cleaner and more maintainable codebases.

Checking Code Formatting

Automated code formatting tools are essential for maintaining code consistency and readability across various programming languages. However, some formatting tools may not indicate formatting issues via their exit status.

Ensuring Proper Formatting in CI

To ensure that your code is properly formatted, you can use a command that checks whether any files need formatting. If the output is empty, it indicates that the code is correctly formatted.

Example Command

Here’s a generic way to check formatting in many programming environments:

test -z "$(<formatter_command>)"

For Go, that’s:

test -z "$(gofmt -l .)"

Breakdown of the Command

  1. <formatter_command>: This command runs the formatting tool in list mode on the current directory and all its subdirectories (e.g. gofmt -l .). If any files need formatting, the tool will print their names without changing them. Don’t use go fmt here: it rewrites the files (so the check would always pass), and ./... is go tool syntax that gofmt doesn’t accept.

  2. **”()syntax is used for command substitution. It runs the command inside the parentheses and replaces$()with its output. Keep the quotes: unquoted, two or more file names become separate arguments andtest` fails with “too many arguments”.

  3. test -z ”$(<formatter_command>)”: The test command checks if the output from the formatting tool is an empty string:

    • -z option returns 0 (true) if the string is empty, indicating that the code is formatted correctly.
    • Returns 1 (false) if there are any files that need formatting.

Practical Steps

  1. Edit a File: Make an intentional formatting mistake in your code to test the formatting check.

  2. Run the Formatting Check: Execute the formatting check command:

    test -z "$(<formatter_command>)"
  3. Check the Exit Code: Use the following command to print the exit code of the last command:

    echo $?
    • You should see 1, indicating that the code is not formatted properly.
  4. Fix Formatting: Run the formatting command again to automatically fix the formatting:

    go fmt ./...   # or: gofmt -w .
  5. Re-check the Formatting: Run the formatting check command again:

    test -z "$(<formatter_command>)"
    echo $?
    • This time, you should see 0, confirming that the code is now properly formatted.

Conclusion

Implementing code formatting checks as part of the CI/CD pipeline helps maintain code quality and consistency across different programming languages. Automating this process can significantly improve the code review process and reduce formatting-related discussions among team members.

Link to original

CICD_Linting

CICD_Linting

CI/CD - Linting

Linting is the process of analyzing code for potential errors, style violations, and other issues. It helps maintain code quality, consistency, and readability across a codebase. Linters are tools that automatically check code for common problems and enforce coding standards.

Linting

Definition: Linting refers to the analysis of source code to detect potential errors, bugs, stylistic errors, and suspicious constructs. Linters provide warnings or errors for potentially problematic code, helping developers maintain code quality.

Key Differences from Code Formatting

  • Code Formatting: Deals with the aesthetic appearance of the code, including aspects like whitespace, indentation, and line length.
  • Linting: Focuses on analyzing code for functional issues and potential bugs, offering insights into code quality.
  • Each programming language typically has its own set of linters. Common examples include:
    • JavaScript/TypeScript: ESLint
    • Python: Flake8, Pylint
    • Java: Checkstyle, PMD
    • Go: Staticcheck (recommended over Golint)

Installation

Installing a Linter: Installation methods may vary based on the language and linter being used. Common approaches include:

  • Package managers (e.g., npm for JavaScript, pip for Python)
  • Direct installation via command line

Example for Go’s Staticcheck:

go install honnef.co/go/tools/cmd/staticcheck@latest

or

brew install staticcheck

Running the Linter

  1. Command to Run: Typically, you run the linter from the command line within your project directory.

    • Example for Go:

      staticcheck ./...
  2. Check for Issues: If there are any issues in your code, the linter will output warnings or errors, often detailing the nature of the problem.

  3. Environment Configuration: Ensure your development environment is configured to recognize the linter, which may involve adjusting environment variables (e.g., PATH).

Example Issue Detection

  • Introduce a common code issue, such as an unused function, to verify that the linter detects it.

Example of an unused function:

func unused() {
    // this function does nothing
    // and is called nowhere
}
  • Upon running the linter, expect to see a warning or error indicating the issue, such as:
func unused is unused (U1000)

Conclusion

Linting is an essential practice in software development, helping to ensure code quality and maintainability. Integrating linting into your development workflow can save time and reduce the likelihood of introducing bugs into your codebase.

CI Linting

Objective: Integrate linting checks into the CI pipeline to ensure code quality by automatically identifying and reporting issues during the build process.

Steps to Integrate Linting in CI

  1. Install the Linter:

    • The CI runner may not have the required linter installed, so add a step in your CI configuration to install the linter specific to your programming language before executing any linting commands.

    Example for Go:

    - name: Install staticcheck
      run: go install honnef.co/go/tools/cmd/staticcheck@latest

    Example for JavaScript (ESLint):

    - name: Install ESLint
      run: npm install eslint

    Example for Python (Flake8):

    - name: Install Flake8
      run: pip install flake8
  2. Run Linting After Formatting:

    • After performing any code formatting checks (e.g., using prettier for JavaScript or black for Python), add a step to run the linter. This ensures both formatting and linting issues are caught in the CI pipeline.

    Example for Go:

    - name: Run staticcheck
      run: staticcheck ./...

    Example for JavaScript:

    - name: Run ESLint
      run: npx eslint .
    • ESLint v9+ uses flat config and removed --ext; which files get linted (e.g. **/*.{js,jsx}) is set by files in eslint.config.js.

    Example for Python:

    - name: Run Flake8
      run: flake8 .
  3. Commit and Push Changes:

    • Introduce a known issue (like an unused variable or function) to verify that the CI workflow correctly identifies and reports the problem. Commit and push these changes to your branch with an open pull request.
  4. Verify CI Workflow Failure:

    • After pushing, check that the CI workflow fails as expected due to the linting error detected by the linter.
  5. Fix the Issue:

    • Resolve the linting errors by modifying the code, and then commit and push your changes again. The CI workflow should now pass without any errors.
  6. Alternative Integration Methods:

    • Consider using official GitHub Actions or other CI/CD tools specific to your programming language. These can simplify the integration process and help manage updates.

Conclusion

Integrating linting checks into your CI pipeline is crucial for maintaining high code quality across programming languages. Automating the detection of potential issues reduces manual review workloads and ensures adherence to established coding standards. By following these steps, you can effectively incorporate linting into your development workflow regardless of the programming language.

Link to original

CICD_Security

CICD_Security

Security Checks in CI/CD Pipelines

Overview
Security checks in Continuous Integration (CI) are automated processes used to identify potential security vulnerabilities in the codebase. They are an essential part of the DevSecOps approach, ensuring that code is scanned for security issues during the development process itself. Static security analysis tools can identify problems like hardcoded secrets, unhandled errors, vulnerable dependencies, and more before code is deployed.

Popular Security Check Tools Across Different Languages
While the provided example uses Gosec for Go, various security tools are available for different programming languages:

  • Go: Gosec
  • JavaScript/TypeScript: ESLint, Node.js Security Scan, Retire.js
  • Python: Bandit, Safety, Pylint
  • Java: SpotBugs, OWASP Dependency Check
  • C#/.NET: Security Code Scan
  • Ruby: Brakeman
  • PHP: Psalm, PHPStan
  • General Purpose: SonarQube, Checkmarx, Semgrep

Using Gosec for Security Checks (Example for Go)

1. Install Gosec
To install Gosec, use the following command:

go install github.com/securego/gosec/v2/cmd/gosec@latest

2. Run Gosec on the Codebase
To run Gosec and analyze the entire codebase, execute:

gosec ./...

Expected Output
When you run Gosec, you might see a list of security warnings, such as unhandled errors or potential vulnerabilities. It’s okay if there are some findings; the objective is to gradually address them and improve security.

Adding Security Checks to CI Workflows

Integrating Security Checks in CI Pipelines
To make security checks part of your CI/CD process, follow these steps:

  1. Install the Security Tool
    In the relevant job step, install the security tool if it’s not built into the runner. For example:

    - name: Install Gosec
      run: go install github.com/securego/gosec/v2/cmd/gosec@latest
  2. Run Security Analysis
    After installing the tool, add a step to run the security analysis:

    - name: Run Gosec
      run: gosec ./...
  3. Handle Results
    CI tools like GitHub Actions or Jenkins can be configured to fail the build if security issues are found. Alternatively, they can just raise a warning depending on the severity.

Best Practices for Security Checks in CI/CD

  1. Run Security Scans Early and Often
    Running scans only at the end of the development process can lead to missed issues. Integrate security scans from the start to catch issues as code is developed.

  2. Set a Baseline
    If the current codebase has many security findings, set a baseline to prevent new vulnerabilities from being introduced while you work on fixing existing ones.

  3. Use a Combination of Tools
    One tool might not catch everything. Consider using multiple tools for broader coverage (e.g., combining static analysis, dynamic analysis, and dependency scanning).

  4. Incorporate Security Tests in Your Development Process
    Security shouldn’t be an afterthought. Incorporate security practices and tools in the local development environment as well to prevent issues from being committed in the first place.

Takeaway

Static security checks are an important step in ensuring the safety of your codebase, and they should be a routine part of the CI/CD pipeline. Implementing automated tools like Gosec (or other language-specific tools) ensures potential vulnerabilities are caught and fixed early, making your application more robust and secure.

Link to original

CICD_Tests

CICD_Tests

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.
Link to original

Notes in this canvas