Cypress micro-frontend architecture on Azure Devops

Cypress micro-frontend architecture on Azure Devops

  • azure devops
  • cypress
  • integration tests
published 2020-02-18

Working on a micro frontend architecture usually means splitting up a monolithic site and its sections into different parts, where each part (or individual site) can be owned and worked on independently by different teams, each with their own product owner, stakeholders and autonomous release trains, and yes you guessed it - integration tests.

Going forward try to imagine a site where the /foo url is owned by team foo, the /bar url by team bar and global tests are owned by both.

In this post we're going to take a deep dive into setting up scheduled integration tests with Cypress and making everything run across multiple pipelines on Azure Devops.

Cypress repo - scaffolding

The project scaffolding for supporting multiple sites is easy, just think of it as a bunch of test commands pointing to different folders like /integration/foo and /integration/bar.

This way each site and team could have their own root folder housing all their tests.

{
  "test:all": "cross-env cypress run",
  "test:foo": "cross-env cypress run --spec './cypress/integration/foo/*'",
  "test:bar": "cross-env cypress run --spec './cypress/integration/bar/*'"
}

Folder structure

├── integration
│   ├── global
│   │   └── foobar.spec.js
│   ├── foo
│   │   ├── foo.visuals.spec.js
│   │   ├── foo.interactions.spec.js
│   ├── bar
│   │   ├── bar.visuals.spec.js
│   │   ├── bar.interactions.spec.js
├── src
│   ├── Settings
│   │   └── index.js
├── support
│   ├── commands.js
│   └── index.js

Handling global settings

For DX, it's important to setup a file that knows about global things like settings and environment variables.

Create a file somewhere in your project and link to it in your Cypress support file

// support / index.js
import "../src/settings";

This is how this settings file could look like:

// src / settings / index.js
const settings = {
  foo: Cypress.env("foo"),
  bar: Cypress.env("bar"),
  randomSetting: 3000,
};
cy.settings = settings;
 
// cy.settings is available everywhere from now on

Cypress looks for a local cypress.env.json file if it cannot find the specified environment variables

{
  "foo": "https://sample-site-qa.com/foo",
  "bar": "https://sample-site-qa.com/bar"
}

A sample test utilizing cy.settings:

context('Foo', () => {
  before(() => {
    cy.visit(cy.settings.foo);
  });
 
  describe('something', () => {
    it('does something successfully', () => {
      // ...
    })
  })
 

Main test build pipeline in Azure - YAML

Step 1 - booting up:

Let's look at the main test pipeline. First of, we set it up to trigger when our master branch changes. Then after requiring NPM we specify that we want to pull in a variables group for QA (more about that further down).

trigger:
  - master
 
pool:
  vmImage: "ubuntu-latest"
  demands: npm
 
variables:
  - group: "site-qa"

Step 2 - scheduling:

Next, we setup a scheduled 45 minute cycle cron job and install the dependencies specified in the package-lock.

The order of the cron syntax goes like this:

  • minutes
  • hours
  • days
  • months
  • days of week

schedules:
  - cron: "*/45 * * * *"
    displayName: "Run once every 45 minutes"
    branches:
      include:
        - master
    always: "true"
 
steps:
  - task: Npm@1
    displayName: "Npm CI"
    inputs:
      command: "custom"
      workingDir: cypress
      verbose: true
      customCommand: "ci"
 
  - task: Npm@1
    displayName: "Npm clean"
    inputs:
      command: "custom"
      workingDir: cypress
      customCommand: "run clean"

Step 3 - environment variables:

In this step we use a script block to grab all variables available within the variable group we imported in step 1 and pass them along to Cypress by prefixing them with CYPRESS_

(doing so makes them available to us within our tests)

Then we run all of our tests, meaning every .spec file that Cypress finds.

The reason to why we want to run all of our tests and not site specific tests is that we want the outcome of running all of our tests to be the predicate for if we publish an artifact or not.

- script: |
    set CYPRESS_foo=$(foo)
    set CYPRESS_bar=$(bar)
  failOnStderr: true
  workingDirectory: cypress
  displayName: "Set Cypress env variables"
 
- task: Npm@1
  displayName: "Npm run test:all"
  inputs:
    command: "custom"
    workingDir: cypress
    customCommand: "run test:all"

Step 4 - publish results and videos

After running our tests we make sure to publish the test results and the captured videos regardless if the test pass or fail.

- task: PublishTestResults@2
  displayName: "Publish Test Results **/test-result-*.xml"
  condition: succeededOrFailed()
  inputs:
    searchFolder: "$(System.DefaultWorkingDirectory)"
    testResultsFormat: "JUnit"
    testResultsFiles: "**/test-result-*.xml"
    failTaskOnFailedTests: false
 
- task: CopyFiles@2
  displayName: "Copy videos"
  inputs:
    SourceFolder: cypress/videos
    TargetFolder: "$(build.artifactstagingdirectory)"
  condition: succeededOrFailed()

Step 5 - archiving and publishing main artifact

Lastly, we publish the main artifict given all test pass.

- task: ArchiveFiles@2
  displayName: "Zip artifact"
  inputs:
    rootFolderOrFile: "$(Build.SourcesDirectory)"
    includeRootFolder: false
    archiveFile: "$(Build.ArtifactStagingDirectory)/Cypress.zip"
 
- task: PublishBuildArtifacts@1
  displayName: "Publish Artifact: cypress-drop"
  inputs:
    PathtoPublish: "$(Build.ArtifactStagingDirectory)"
    ArtifactName: "cypress-drop"
    publishLocation: "Container"

Sample site build pipeline for foo / bar

This is how the testing phase of one of our micro sites could look:

example

This phase would typically be after the deployment phase so that the tests are run on the live environment.

As you see on the image, we're using an Azure task group since the only thing different between our test phase for our foo and bar pipelines are which tests we should run (notice the npm script)

Variable and task groups

Variable / task groups are great when you need to reuse functionality across multiple pipelines. Let's have a look at how both of these work.

Variable groups

You create a variable group under Pipelines -> Library -> Variable groups.

In our case we create two:

  • site-qa
  • site-prod

Remember our script phase in our YAML file? This is where the variables foo and bar are specified and given different values depending on the environment.

variable groups

Task groups

A task group is a set of reusable commands, written in either GUI blocks or with YAML.

As I wrote earlier, the reason to why we need one of these is because we're going to have multiple pipelines and want to avoid writing the same thing over and over again. Right now we only have the foo and bar sites, but in the future this could scale into more sites.

Our task group will test one of our sites based on a dynamic argument, more specifically:

  • Download and extract the latest artifact
  • Run npm CI
  • Run a specific test script (command line)
  • Publish test results

The dynamic element to our test group is setup like this

set CYPRESS_foo=$(foo)
set CYPRESS_bar=$(bar)
 
npm run $(npm.script)

As you see from the code above, the task group is dependant on the pipeline using it to not only include the appropriate variable group but to also specify which test script the task group should initiate.

Trigger tests on other environments

Remember that if everything goes as planned and all of our tests pass, the YAML file has instructions to create and publish a second artifact named cypress-drop.

This artifact is what you could use as a trigger to setup a release pipeline that could run your tests against some other environment like prod or pre-prod, just remember to load in the correct variable group.

Summary

The idea behind this post was to show a simple way of having one single repo host different tests for multiple sites and connecting the dots in Azure Devops.