Avatar

Blog

  • Published on
    When building CI/CD pipelines it is often the case that you'd like to "tokenize" a configuration file, so that the values in the file can be calculated during the build/release execution process. This can be for all kinds of reasons, such as ingesting environment variables or by basing values on the outputs of other CLI tools, such as terraform (e.g. reading IDs of recently created infrastructure). Rather than separating the key/value "tokens" from the "token calculation" logic, I wanted a way to embed Bash scripts directly into my JSON file and then have interpret the values into an output file. For example, the JSON file might look like:
    
    [
        {
            "Key": "verbatim",
            "Value": "just a string"
        },
        {
            "Key": "environment-variable",
            "Value": "$HOME"
        },
        {
            "Key": "shell-script",
            "Value": "$(dotnet --version)"
        }
    ]
    
    With the invokable tokens defined, the below script can be run against the JSON file in order to parse and execute the tokens:
    
    #!/bin/bash
    
    jsonFile=$1
    tempFile=$2
    
    echo "[" > "$tempFile"
    
    itemCount=$(jq '. | length' $jsonFile)
    currentIndex=0
    
    # Read and process the JSON file line by line.
    jq -c '.[]' $jsonFile | while read -r obj; do
        key=$(echo "$obj" | jq -r '.Key')
        value=$(echo "$obj" | jq -r '.Value')
    
        # Check if value needs command execution
        if [[ "$value" == \$\(* ]]; then
            # Remove $() for command execution
            command=$(echo "$value" | sed 's/^\$\((.*)\)$/\1/')
            newValue=$(eval $command)
        elif [[ "$value" == \$* ]]; then
            # It's an environment variable
            varName=${value:1} # Remove leading $
            newValue=${!varName}
        else
            # Plain text, no change needed
            newValue="$value"
        fi
    
        # Update the JSON object with the new value
        updatedObj=$(echo "$obj" | jq --arg newValue "$newValue" '.Value = $newValue')
    
        # Append the updated object to the temp file
        echo "$updatedObj" >> "$tempFile"
    
        # Add a comma except for the last item
        ((currentIndex++))
        if [[ $currentIndex -lt $itemCount ]]; then
            echo ',' >> "$tempFile"
        fi
    done
    
    echo "]" >> "$tempFile"
    
    Running the command looks like
    ./json_exec.sh example.json example.out.json
    And the output then looks as follows:
    
    [
    {
      "Key": "verbatim",
      "Value": "just a string"
    }
    ,
    {
      "Key": "environment-variable",
      "Value": "/home/craig"
    }
    ,
    {
      "Key": "shell-script",
      "Value": "8.0.202"
    }
    ]
    
  • Published on
    Imagine you want to create a very generic SpecFlow step definition that can be used to verify that a certain HttpMessageRequest was sent by your system that uses HttpClient. You want to check your system calls the expected endpoint, with the expected HTTP method and that the body data is as expected. The gherkin syntax for the method might be something like:
    Then the system should call 'POST' on the 3rd party 'hello-world' endpoint, with the below data
      | myBodyParam1 | myBodyParam2 |
      | Hello        | World        |
    
    C# being a strongly typed language, it's actually not that straightforward to make a robust comparison of the JSON that was sent in a request, with a Table that is supplied to SpecFlow. However, I did manage to come up with such a way, which is documented below.
    
    [Then(@"the system should call '(.*)' on the 3rd party '(.*)' endpoint, with the below data")]
        public void ThenTheSystemShouldCallOnThe3rdPartyEndpointWithTheBelowData(HttpMethod httpMethod, string endpointName,
            Table table)
        {
            var expectedRequest = table.CreateDynamicInstance();
    
            _mock.VerifyRequest(httpMethod, 
              async message => message.RequestUri!.AbsoluteUri.EndsWith(endpointName) &&
                await FluentVerifier.VerifyFluentAssertion(async () =>
                  JsonConvert.DeserializeAnonymousType(
                    await message.Content!.ReadAsStringAsync(),
                    expectedRequest).Should().BeEquivalentTo(expectedRequest)));
        }
    
    There's several parts to the magic, in order:
    1. `table.CreateDynamicInstance` - this extension comes from the SpecFlow.Assist.Dynamic NuGet package, which allows you to create an anonymous type instance from a SpecFlow table.
    2. `_mock.VerifyRequest` - this extension comes from the Moq.Contrib.HttpClient, which isn't strictly necessary but is a nice way to manage your HttpClient's mocked message handler and make assertions on it.
    3. `await FluentVerifier.VerifyFluentAssertion` - uses this trick for making FluentAssertions inside of a Moq Verify call (so you can use equivalency checks rather than equality).
    4. `JsonConvert.DeserializeAnonymousType` - allows you to deserialize JSON to an anonymous type based on a donor "shape" (which we get from the "expected" anonymous type)
  • Published on
    Cypress is a great choice of tool for writing e2e UI automation tests for web applications. One of things you'll invariably want to do when writing tests is stub the dependencies of your system, so that you can isolate the SUT and allow for performant test runs. If you're testing a purely client side code-base, like React, then the built-in cy.intercept might do the job. This will intercept calls made from the browser to downstream APIs and allows you to stub those calls before it leaves the client. However for a Next.js application that includes server side data fetching (e.g. during SSR) or where you have implemented Next.js APIs (e.g. for "backend for frontend" / "edge" APIs) that you want to include as part of the "system under test", you need another option. The way the Cypress Network Requests documentation reads, it seems like the only choices are mocking in the browser using cy.intercept, or spinning up your entire dependency tree - but there is a 3rd option - mocking on the server. Mocking server side calls isn't a new paradigm if you're used to automation testing C# code or have used other UI testing frameworks so I won't go into major detail on the topic, but I wanted to write this article particularly for Cypress as the way you interact with the external mocks is different in Cypress. To mock a downstream API, you can spin up stub servers that allow you to interact with them remotely such as WireMock or in this case "mockserver". What you need to achieve will comprise of these steps:
    1. Before the test run - spin up the mock server on the same address/port as the "real" server would run (actually you can change configs too by using a custom environment, but to keep it simple let's just use the default dev setup)
    2. Before the test run - spin up the system under test
    3. Before an "act" (i.e. during "arrange") you want to setup specific stubs on the mock server for your test to use
    4. During an "assert" you might want to verify how the mock server was called
    5. At the end of a test run, stop the mock server to free up the port
    In order to orchestrate spinning up the mock server and the SUT, I'd recommend scripting the test execution - which you can read more about here - below shows an example script to achieve this: test-runner.mjs
    
    #!/usr/bin/env node
    import { subProcess, subProcessSync } from 'subspawn';
    import waitOn from 'wait-on';
    const cwd = process.cwd();
    
    // automatically start the mock
    subProcess('test-runner', 'npm run start-mock', true);
    await waitOn({ resources: ['tcp:localhost:5287'], log: true }, undefined);
    
    // start the SUT
    process.chdir('../../src/YourApplicationRoot');
    subProcess('test-runner', 'make run', true);
    await waitOn({ resources: ['http://localhost:3000'] }, undefined);
    
    // run the tests
    process.chdir(cwd);
    subProcessSync("npm run cy:run", true);
    
    // automatically stop the mock
    subProcess('test-runner', 'npm run stop-mock', true);
    
    process.exit(0);
    
    That suits full test runs and CI builds, but if you're just running one test at a time from your IDE you might want to manually start and stop the mock server from the command line, which you can do by running the "start-mock" and "stop-mock" scripts from the CLI, hence why they have been split out. start-mock.js
    
    const mockServer = require('mockserver-node');
    
    mockServer.start_mockserver({ serverPort: 5287, verbose: true })
      .then(() => {
        console.log('Mock server started on port 5287');
      })
      .catch(err => {
        console.error('Failed to start mock server:', err);
      });
    
    stop-mock.js
    
    const mockServer = require('mockserver-node');
    
    mockServer.stop_mockserver({ serverPort: 5287 })
      .then(() => {
        console.log('Mock server stopped');
      })
      .catch(err => {
        console.error('Failed to stop mock server:', err);
      });
    
    package.json:
    
    "scripts": {
        "start-mock": "node start-mock.js",
        "stop-mock": "node stop-mock.js",
        "test": "node test-runner.mjs",
        "cy:run": "cypress run"
      },
    
    With the mock server and SUT running you can now interact with them during your test run, however in Cypress the way to achieve this is using custom tasks. Below shows an example task file that allows you to create and verify mocks against the mock-server: mockServerTasks.js
    
    const { mockServerClient } = require('mockserver-client');
    
    const verifyRequest = async ({ method, path, body, times = 1 }) => {
      try {
        await mockServerClient('localhost', 5287).verify({
          method: method,
          path: path,
          body: {
            type: 'JSON',
            json: JSON.stringify(body),
            matchType: 'STRICT'
          }
        }, times);
        return { verified: true };
      } catch (error) {
        console.error('Verification failed:', error);
        return { verified: false, error: error.message };
      }
    }
    
    const setupResponse = ({ path, body, statusCode }) => {
      return mockServerClient('localhost', 5287).mockSimpleResponse(path, body, statusCode);
    };
    
    module.exports = { verifyRequest, setupResponse };
    
    This can then be imported into your Cypress config:
    
    const { defineConfig } = require("cypress");
    const { verifyRequest, setupResponse } = require('./cypress/tasks/mockServerTasks');
    
    module.exports = defineConfig({
      e2e: {
        setupNodeEvents(on, config) {
          on('task', { verifyRequest, setupResponse })
        },
        baseUrl: 'http://localhost:3000'
      },
    });
    
    Finally, with the custom tasks registered with Cypress, you can use the mock server in your tests, e.g.:
    
    it('correctly calls downstream API', () => {
        // setup API mock
        cy.task('setupResponse', { path: '/example', body: { example: 'response' }, statusCode: 200 }).should('not.be.undefined');
    
        // submit the form (custom command that triggers the behaviour we are testing)
        cy.SubmitExampleForm();
        
        cy.contains('Example form submitted successfully!').should('be.visible');
    
        // verify API call
        cy.task('verifyRequest', {
          method: 'POST',
          path: '/example',
          body: {
             example: 'request'
          },
          times: 1
        }).should((result) => {
          expect(result.verified, result.error).to.be.true;
        });
      });
    
  • Published on
    I like to work in pictures - and I use diagrams A LOT as a way to express the problem or solution in a digestible format. A picture is worth a thousand words, after all! In the early stages of understanding, I find it's best to stay in a diagramming tool like "draw.io" - you can quickly throw things onto a page, often in real-time while collaborating on a screen share and let the diagram drive the narrative of the conversation. The outputs of these diagrams can be easily shared for all to see and can quickly build up a dossier of useful documentation when it comes to explaining or coding the solution down the line. I tend not to worry so much at this stage about following any particular "style" of diagram, with draw.io supporting everything from swim lanes, flow charts, deployment diagrams, component diagrams and more, I usually go for whatever combination is the most useful or informative for the situation. The important measure for a diagram is that everyone, who needs to, can understand it. As you get closer to an agreed architecture, there comes a point where it's worth moving to a "model" over a "diagram" (by that I mean not just pictures, but written syntax that can be output as diagrams - i.e. "documentation as code"). What springs to mind here is PlantUML (or Mermaid diagrams) which allow you to define a plethora of diagram types using written syntax. The main benefit of this in my experience is change control, as written syntax plays very nicely with source control systems, such as Git so you can request peer reviews of documentation changes, keep it aligned with code changes and can see the history of changes with commit messages as to why things changed. As a "standard" set of diagrams to get started, I'd recommend the "C4 Model" - this will give you a good repeatable basis for capturing the system architecture at various levels of abstraction. I don't really recommend going beyond components and even then, I'd use that level of detail sparingly. However, both PlantUML and Mermaid support multiple other diagram types so it's worth having a dig through to see what you find useful. I especially like to create sequence diagrams as text, as I find that easier than doing it graphically. You can even graphically represent JSON or YAML data, create pie charts and more! I tend to categorise documentation into "specific" or "overarching" - "specific" documentation you'd find in the Git repo of the specific system it relates to, typically in a "/docs" folder alongside "/src" and here you will find the software and solution level documentation focused on the internal workings of this system, with perhaps minimal references to the external systems it interacts with (e.g. at the "context" level). Some good examples would be the lower level C4 diagrams, sequence or flow diagrams etc. Sometimes though, documentation focuses on the interaction between systems rather than a specific system, or it's documenting the business processes using mind maps etc. and isn't "about" any particular API. In that case to me that's "overarching" documentation and I'd have a dedicated repo for that genre of documentation. Unitl recently, I was using an open source tool called "C4Builder" to manage my documentation projects. It allows you to setup a structure of "md" (markdown) and "puml" (plantUML) files and then generate them into a HTML/Image output, which can be hosted directly in the Git repository. There are plugins for VS Code and JetBrains Rider that allow you to code Markdown/PlantUML with previews, while you are working on the documentation, which c4builder can then "compile". However, this tool seems to no longer be maintained so it doesn't support the latest version of PlantUML which limits what diagrams and syntax are available to you - I have created a Docker image version that monkey patches in the latest (at the time of writing) PlantUML so you can at least continue to build .c4builder projects, but I've now discovered a new way to manage my documenation: JetBrains Writerside. Writerside is still in EAP and has just announced support for PlantUML, it already supports Mermaid diagrams and has a tonne of other useful features for creating and publishing technical documentation, such as supporting OpenAPI specs and markup/markdown elements so it's well worth a look! To sum up, I recommend creating diagrams throughout the software development process, including during requirements gathering to visualise and get a collective understanding of the problem and agreement on proposed solution(s). As you move towards starting to write code, formalise the necessary (not everything) documentation into a syntactic model that is peer reviewed and source controlled. Store this either alongside the code, or in an "overarching" architecture repository. Ensure your CI/CD process automatically publishes the latest documentation when it changes. Finally, make sure the documentation you are creating has a purpose and stays up to date, if it's old or no longer serves a purpose - archive it!
  • Published on
    Makefiles are as old as time in the world of software development, but over the years their usefulness has waned due to the rise of IDEs and project files like .csproj/.sln in dotnet, or package.json in Node. However recently I've found myself wanting to use them again and here's why:
    1. I have projects that span multiple technologies (e.g. dotnet back-end, Node.js based front-end, terraform/dockerfile/helm IaC, documentation as code etc.).
      • From a single `make run` entrypoint, you can spin up multiple systems in parallel, such as calling `dotnet run` and `npm run dev`.
      • Similarly you can create a single `make test` that will run all tests across tech stacks.
      • Even with a single technology, like dotnet, often one solution can contain an API plus one or more serverless functions so the startup projects can easily adjusted.
      • Makefiles can call other makefiles, so you can create specialised "makefiles" within things like "/src/backend", "/src/frontend", "/docs", "/build", "/tests" etc. and then call them from the "uber makefile" at repo level (similar to the workspace concept)
      • Make supports bash completion, so you can quickly see what targets are supported
    2. I have dependencies that are outside of the development environment (e.g. docker compose files to spin up containers).
      • Creating a make target to spin up your Docker Compose containers means you never have to remember to do it manually before starting the project.
      • Some containers themselves have dependencies, for example passing in a .pfx file, which you can also automate the creation of when it doesn't yet exist.
      • You can call CLIs that are outside of the scope of your development technology, such as running ngrok to serve external traffic.
    3. I have a mixture of package managers/CLI tools used to build things, and want a "standard" way to build (e.g. npm, pnpm, yarn, dotnet, docker, helm).
      • When you're hopping between many different CLI tools from `dotnet`, `npm` (and the variants), `docker`, `expo`, `tf` and the rest, it's easy to forget the build syntax for the particular project - rather than putting that into a README, just put it into the makefile!
      • It's a lot easier to remember `make build` than, [p]npm/yarn/dotnet/docker/tf/helm build --Flag1 --Flag2 etc.
    4. I like that I can automatically run `npm i` whenever it needs to run (e.g. only when the package.json has changed).
      • By making the target "node_modules" depend on "package.json", Make will only run the script, e.g. `npm i`, when package.json has changed, or if node_modules doesn't exist.
    5. I can implement and test my build pipeline locally, then just call `make build` or `make test` from the build server.
      • Building for production usually at least requires calling some CLI tool with the correct arguments (e.g. release configuration, code coverage settings, environment variables etc.).
      • Making the build/test script run consistently between local and build servers make it much easier to debug.
    Of course there are other technologies at your disposal, such as using your .sln file configurations and build targets, using npm workspaces, using scripting languages like bash/powershell/JavaScript, using Dockerfiles to do everything. I've settled on makefiles being a good/standard technology stack agnostic "entry point" which can then tap into the other tooling for the specific project, even it that's just "turbo" which takes care of the rest. In my experience, 9 times out of a 10 a Git repo will end up with at least 2 different CLI tools required to build everything when you factor in source code, documentation, infrastructure as code, tests, CI/CD pipelines. If you're thinking you might give it a try there's some things to watch out for:
    1. "Make" isn't Docker, so you can't guarantee the tools you're calling in your scripts are installed.
      • This is no different to anything else, whether a README full of commands, and .sh/.ps1/.mjs file full of commands, or even a ".sln" file requires something be installed first.
      • Just be sensible to use the "common tooling" of ecosystem you expect developers to have (npm/node/dotnet/docker for example)
    2. Makefiles aren't bash, so be careful of syntax
      • They look a lot like bash, but they're not - if you want to use the shell you'd need to wrap the code in $(shell COMMAND), but be careful of cross-platform issues as it still might not be bash!
      • This means bash scripts aren't always lift-n-shift into makefiles
    3. Phony targets always run
      • Since they're not backed by a physical file, "make" can't compute if something has "changed", so it has to run them
      • This could mean you're running things unnecessarily multiple times, which isn't a problem if the underlying tool is idempotent or performant (e.g. running `docker compose up -d` when it's already running doesn't really do much)
    4. Consider cross platform usage
      • Make comes with most Linux distros, but not Windows and probably not iOS - so if you're sharing a repo with multiple OS's you need to consider if "make" is still the best tool
    Here's are a couple of example extracts of makefiles. Example 1: Shows targets for running a dotnet API, with some Azure Functions (an Event Grid handler and an Edge Api), ngrok, spinning up a Docker Compose project as well as creating a shared .pfx file mounted into a container:
    
    .PHONY: run
    .PHONY: run-api
    .PHONY: run-containers
    .PHONY: run-EventGridHandler.ExampleHandler
    .PHONY: run-ExampleEdge
    .PHONY: run-ExampleEdge-api
    .PHONY: run-ExampleEdge-ngrok
    
    # create the local dev cert that can be used by the event grid simulator
    .docker/azureEventGridSimulator.pfx:
    	dotnet dev-certs https --export-path .docker/azureEventGridSimulator.pfx --password ExamplePW
    
    run-api: run-containers
    	cd Api && dotnet run
    	
    run-EventGridHandler.ExampleHandler: run-containers
    	cd Func.EventGridHandler.ExampleHandler && func start
    		
    run-ExampleEdge:
    	@$(MAKE) -j 2 run-ExampleEdge-api run-ExampleEdge-ngrok
    
    run-ExampleEdge-api: run-containers
    	cd Func.ExampleEdge && func start --port 7073
    	
    run-ExampleEdge-ngrok:
    	ngrok http --domain=example.ngrok-free.app 7073	> /dev/null
    	
    run-containers: .docker/azureEventGridSimulator.pfx
    	docker compose up -d
    
    # run everything
    run:
    	dotnet build && \
    	make -j 3 run-api run-EventGridHandler.ExampleHandler run-ExampleEdge
    
    Developers can run `make <target>` for specific projects only, or run `make run` to spin up everything. Example 2: Shows building two front-end projects, in production mode and copying static assets to build output of Next.js
    
    .PHONY: run
    .PHONY: build
    .PHONY: build-widget
    .PHONY: build-site
    
    nextjs-website/node_modules: nextjs-website/package.json
    	cd nextjs-website && pnpm i
    
    build-site: nextjs-website/node_modules
    	cd nextjs-website && export NODE_ENV=production && pnpm run build && cp -r .next/static .next/standalone/.next/static && cp -r public .next/standalone
    
    build:
    	@$(MAKE) -j 2 build-site build-widget 
    
    javascript-widget/node_modules: javascript-widget/package.json
    	cd javascript-widget && pnpm i
    	
    javascript-widget: javascript-widget/node_modules
    	cd javascript-widget && export NODE_ENV=production && pnpm run build
    
    The build pipeline simply has to `corepack enable && make build` without worrying about correctly assembling the outputs. In summary, "make" provides a convenient way to package up "commands" as "targets" and then create a dependency graph amongst those targets, such that running a single command can daisy chain together all of the pre-requisites. It's mainly suited to Linux/Unix so can be very useful to create "CI/CD provider agnostic" build scripts (i.e. not using provider tasks) that can then be called from the provider yaml pipelines and/or can be used locally to execute the build steps or to spin up multi-faceted solutions easily.