Showing posts with label azure dev ops. Show all posts
Showing posts with label azure dev ops. Show all posts

Monday, December 20, 2021

Azure DevOps CI/CD Pipeline — JAVA

 

Here, For the beginners I explained about “How to implement Azure DevOps Pipeline”

Image Copied from Azure

Introduction

Using tools like Jenkins does not mean you follow CI/CD.

Continuous Integration (CI) is a development practice. In which developers integrate code into a shared repository frequently, preferably several times a day. An automated build or automated tests should verify each integration build.

Mostly, we follow below flow for CI/CD.

Above image copied from “Release It! Design and Deploy by Michael Nygard”

In Continuous integration — For individual commits — the Azure pipeline will build a container image (prefer docker image), then test it and push it to container registry.

Create Azure Devops Project

Create new repository

*** Refer to the last section for cloning a repository from your Github account.

|
`-- microservice1
|-- Dockerfile
|-- pom.xml
`-- src
|-- main
| |-- java
| | `-- edu
| | `-- sampleapplication
| | `-- MS1
| | |-- ApiController.java
| | `-- Ms1Application.java
| `-- resources
| |-- application.properties
| `-- logback-spring.xml
`-- test
`-- java
`-- edu
`-- sampleapplication
`-- MS1
`-- Ms1ApplicationTests.java

Build pipeline

Do click “Set up Build button” for building a pipeline.

Here, I am selecting an option to build a docker image and push it to the Azure container registry.

Azure pipeline —

Default pipeline YAML file will contain stages like build with the steps or tasks to push the docker image to the container registry.

# Docker
# Build and push an image to Azure Container Registry
# https://docs.microsoft.com/azure/devops/pipelines/languages/docker
trigger:
- master
resources:
- repo: self
variables:
# Container registry service connection established during pipeline creation
dockerRegistryServiceConnection: '8eb2472c-86b9-4669-871b-9b9872670821'
imageRepository: 'springazuredemo'
containerRegistry: 'ritresh.azurecr.io'
dockerfilePath: '$(Build.SourcesDirectory)/microservice1/Dockerfile'
tag: '$(Build.BuildId)'

# Agent VM image name
vmImageName: 'ubuntu-latest'
stages:
- stage: Build
displayName: Build and push stage
jobs:
- job: Build
displayName: Build
pool:
vmImage: $(vmImageName)
steps:
- task: Docker@2
displayName: Build and push an image to container registry
inputs:
command: buildAndPush
repository: $(imageRepository)
dockerfile: $(dockerfilePath)
containerRegistry: $(dockerRegistryServiceConnection)
tags: |
$(tag)
We need to add maven tasks to build the spring boot application as default conf won’t work for our set up.
# Docker
# Build and push an image to Azure Container Registry
# https://docs.microsoft.com/azure/devops/pipelines/languages/docker
trigger:
- master
resources:
- repo: self
variables:
# Container registry service connection established during pipeline creation
dockerRegistryServiceConnection: 'c1a4b0b1-bbda-4066-810a-a3b646af911f'
imageRepository: 'springazuredemo'
containerRegistry: 'ritresh.azurecr.io'
dockerfilePath: '$(Build.SourcesDirectory)/microservice1/Dockerfile'
tag: '$(Build.BuildId)'

# Agent VM image name
vmImageName: 'ubuntu-latest'
stages:
- stage: Build
displayName: Build and push stage
jobs:
- job: Build
displayName: Build
pool:
vmImage: $(vmImageName)
steps:
- task: Maven@3
inputs:
mavenPomFile: 'microservice1/pom.xml'
mavenOptions: '-Xmx3072m'
javaHomeOption: 'JDKVersion'
jdkVersionOption: '1.8'
jdkArchitectureOption: 'x64'
publishJUnitResults: false
goals: 'clean install'
- task: Docker@2
displayName: Build and push an image to container registry
inputs:
command: buildAndPush
repository: $(imageRepository)
dockerfile: $(dockerfilePath)
containerRegistry: $(dockerRegistryServiceConnection)
tags: |
$(tag)

Azure container registry —

Let’s check the Azure container registry, here we could see the newly build docker image under springazuredemo repository.

Lets Run it on local environment

:study ritgirdh$ docker login ritresh.azurecr.io
Username: ritresh
Password:
Login Succeeded
:study ritgirdh$ docker pull ritresh.azurecr.io/springazuredemo:17
17: Pulling from springazuredemo
e7c96db7181b: Already exists
f910a506b6cb: Already exists
c2274a1a0e27: Already exists
8b7b20a1fb06: Pull complete
Digest: sha256:13f46fce6f2910c6fe84e9ff8459ab1a2973740df442717c99c79a9016fe1caa
Status: Downloaded newer image for ritresh.azurecr.io/springazuredemo:17
ritresh.azurecr.io/springazuredemo:17
WKMIN1307242:study ritgirdh$ docker run -p5555:8080 ritresh.azurecr.io/springazuredemo:17
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.0.0.RELEASE)
2020-05-06 11:55:45.969 INFO 1 --- [ main] e.sampleapplication.MS1.Ms1Application : Starting Ms1Application v1.0.0-SNAPSHOT on 8c5ac374947d with PID 1 (/ms1.jar started by root in /)
2020-05-06 11:55:45.983 INFO 1 --- [ main] e.sampleapplication.MS1.Ms1Application : No active profile set, falling back to default profiles: default
2020-05-06 11:55:46.086 INFO 1 --- [ main] ConfigServletWebServerApplicationContext : Refreshing org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@38cccef: startup date [Wed May 06 11:55:46 GMT 2020]; root of context hierarchy
2020-05-06 11:55:48.285 INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2020-05-06 11:55:48.353 INFO 1 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2020-05-06 11:55:48.353 INFO 1 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/8.5.28
2020-05-06 11:55:48.381 INFO 1 --- [ost-startStop-1] o.a.catalina.core.AprLifecycleListener : The APR based Apache Tomcat Native library which allows optimal performance in production environments was not found on the java.library.path: [/usr/lib/jvm/java-1.8-openjdk/jre/lib/amd64/server:/usr/lib/jvm/java-1.8-openjdk/jre/lib/amd64:/usr/lib/jvm/java-1.8-openjdk/jre/../lib/amd64:/usr/java/packages/lib/amd64:/usr/lib64:/lib64:/lib:/usr/lib]
2020-05-06 11:55:48.579 INFO 1 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2020-05-06 11:55:48.579 INFO 1 --- [ost-startStop-1] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 2499 ms
2020-05-06 11:55:49.257 INFO 1 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean : Servlet dispatcherServlet mapped to [/]
2020-05-06 11:55:49.265 INFO 1 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'characterEncodingFilter' to: [/*]
2020-05-06 11:55:49.265 INFO 1 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2020-05-06 11:55:49.265 INFO 1 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'httpPutFormContentFilter' to: [/*]
2020-05-06 11:55:49.266 INFO 1 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'requestContextFilter' to: [/*]
2020-05-06 11:55:49.266 INFO 1 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'httpTraceFilter' to: [/*]
2020-05-06 11:55:49.266 INFO 1 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'webMvcMetricsFilter' to: [/*]
2020-05-06 11:55:49.792 INFO 1 --- [ main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@38cccef: startup date [Wed May 06 11:55:46 GMT 2020]; root of context hierarchy
2020-05-06 11:55:49.923 INFO 1 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/message]}" onto java.lang.String edu.sampleapplication.MS1.ApiController.getMessage()
2020-05-06 11:55:49.925 INFO 1 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/v1],methods=[GET]}" onto public java.lang.String edu.sampleapplication.MS1.ApiController.test()
2020-05-06 11:55:49.929 INFO 1 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2020-05-06 11:55:49.930 INFO 1 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2020-05-06 11:55:50.015 INFO 1 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2020-05-06 11:55:50.015 INFO 1 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2020-05-06 11:55:50.095 INFO 1 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2020-05-06 11:55:50.938 INFO 1 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup
2020-05-06 11:55:50.946 INFO 1 --- [ main] ConfigServletWebServerApplicationContext : Refreshing org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@6c779568: startup date [Wed May 06 11:55:50 GMT 2020]; parent: org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@38cccef
2020-05-06 11:55:51.012 INFO 1 --- [ main] o.s.b.f.s.DefaultListableBeanFactory : Overriding bean definition for bean 'handlerExceptionResolver' with a different definition: replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration; factoryMethodName=handlerExceptionResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration.class]] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.actuate.autoconfigure.web.servlet.WebMvcEndpointChildContextConfiguration; factoryMethodName=compositeHandlerExceptionResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfiguration.class]]
2020-05-06 11:55:51.097 INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8001 (http)
2020-05-06 11:55:51.099 INFO 1 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2020-05-06 11:55:51.100 INFO 1 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/8.5.28
2020-05-06 11:55:51.122 INFO 1 --- [ost-startStop-1] o.a.c.c.C.[Tomcat-1].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2020-05-06 11:55:51.123 INFO 1 --- [ost-startStop-1] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 177 ms
2020-05-06 11:55:51.180 INFO 1 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean : Servlet dispatcherServlet mapped to [/]
2020-05-06 11:55:51.256 INFO 1 --- [ main] s.b.a.e.w.s.WebMvcEndpointHandlerMapping : Mapped "{[/actuator/health],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
2020-05-06 11:55:51.258 INFO 1 --- [ main] s.b.a.e.w.s.WebMvcEndpointHandlerMapping : Mapped "{[/actuator/info],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
2020-05-06 11:55:51.260 INFO 1 --- [ main] s.b.a.e.w.s.WebMvcEndpointHandlerMapping : Mapped "{[/actuator],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto protected java.util.Map<java.lang.String, java.util.Map<java.lang.String, org.springframework.boot.actuate.endpoint.web.Link>> org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping.links(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2020-05-06 11:55:51.334 INFO 1 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public java.util.Map<java.lang.String, java.lang.Object> org.springframework.boot.actuate.autoconfigure.web.servlet.ManagementErrorEndpoint.invoke(org.springframework.web.context.request.ServletWebRequest)
2020-05-06 11:55:51.342 INFO 1 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2020-05-06 11:55:51.343 INFO 1 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2020-05-06 11:55:51.377 INFO 1 --- [ main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@6c779568: startup date [Wed May 06 11:55:50 GMT 2020]; parent: org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@38cccef
2020-05-06 11:55:51.585 INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8001 (http) with context path ''
2020-05-06 11:55:51.615 INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2020-05-06 11:55:51.619 INFO 1 --- [ main] e.sampleapplication.MS1.Ms1Application : Started Ms1Application in 6.497 seconds (JVM running for 7.807)

Lets test it on local —

curl -ivk localhost:5555/v1   
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 5555 (#0)
> GET /v1 HTTP/1.1
> Host: localhost:5555
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200
HTTP/1.1 200
< Content-Type: text/plain;charset=UTF-8
Content-Type: text/plain;charset=UTF-8
< Content-Length: 13
Content-Length: 13
< Date: Wed, 06 May 2020 11:58:35 GMT
Date: Wed, 06 May 2020 11:58:35 GMT
<
* Connection #0 to host localhost left intact
OKHello world

Clone existing repo from Github

Here, I am cloning existing github repo https://github.com/RitreshGirdhar/SpringBoot-Docker-Ansible. And for this demo I have deleted some of the files related to ansible which don’t belong to this Azure-devops demo.

The only purpose of this article is to demonstrate the purpose of Continuous Integration.

We could Configure the Azure pipeline for every Merge request or pull request.

It will build the application and run the default test cases. Once the application gets build successfully, it will containerize the application and push it to the specified container registry.

Later, the pipeline would deploy the application on some azure server or some other service via ansible or native commands.

Thursday, November 18, 2021

Test your application using Cypress, GitHub, and Azure DevOps

 Summary: 

1. create project with product curd operations. 
2. write cypress tests to test fucntionality.
3. Execute tests locally .
4. Execute test using Azure dev ops.
5. Execute tests using Git Ations. 

Very interesting comparision of Azure devops and Git actionaa and writing simple tests using cypress.

Testing is a critical activity that allows development teams to get early feedback about the correctness of their code and eliminate more issues in the early stages of the development cycle. Today’s article will talk about front-end testing with Cypress and its integration with Azure DevOps and GitHub.

What is Cypress?

Cypress is a free, open-source testing tool that runs on a Node.js process and allows a developer to write performance End-to-end, Integration, and unit tests using Mocha’s syntax in JavaScript.

It has two main components that come default in its installation:

  • Cypress Test Runner: Runner that executes your actual test cases.
  • Dashboard: Service that tracks and provides insight about how your tests ran.

How it works?

When you run a test using Cypress test runner, a Node.js process starts and both your application and the test code are embedded into two different iFrames in a browser managed by Cypress. The test’s code communicates with a Node.js process via WebSockets and acts as a proxy by intercepting every HTTP request from the application and then allows Cypress to mock the responses quickly.

Process Explorer

The ability to mock responses isn’t the only benefit of this architecture. By running on along your application, Cypress can access each DOM (document object model) element, the window element, and even when your test needs to perform a command like ‘click a button’, will send the command to the DOM element by using a DOM event directly instead of relying on out-of-process communication with a WebDriver.

Folder structure

After adding a new project, Cypress will automatically create the following folder structure:

.
├── cypress
│ ├── fixtures
│ │ └── data.json
│ ├── integration
│ │ └── test.js
│ ├── plugins
│ │ └── index.js
│ └── support
│ ├── commands.js
│ └── index.js
└── cypress.json

Let’s take a minute to analyze each of them:

  • Fixtures: Store static data that your tests can use.
  • Integration: Contains all test files.
  • Plugins: Special file that executes before the project is loaded and before the browser launches. During your test, execution that modifies or extends Cypress’s internal behavior.
  • Support: This folder contains files with reusable behavior, like custom commands, that run before every spec file.
  • Cypress.json: This file allows you to modify the Cypress’ default behavior by supplying specific configurations like base URL, timeouts, etc.

Installation

If you are developing an application based on Node.js, you can easily add Cypress as a dependency to your package.json by executing the following command on our project root directory.

npm install cypress --save-dev

In case you are using Yarn as a package manager, the command is the following:

yarn add cypress --dev

If you don’t want to add any dependency to your project or your project is based on programming languages different from Node.js, for example, Python and Go; you can still enjoy the Cypress testing tool by running the Test Runner inside a Docker container while running the website on the host outside the container. After starting your application on the host machine, run the Docker image with the following parameters:

DISPLAY=$IP:0
docker run
-it \
-v $PWD:/e2e \
-w /e2e \
--entrypoint cypress \
cypress/included:3.2.0 open --project . \
--config baseUrl=http://host.docker.internal:2222

By doing so, the test runner will point back at the host machine, and you can test your application.

Setup you local environment

Before moving forward with its integration with Azure DevOps and GitHub, clone the repository from the following link.

I mainly use this repository for demo purposes. In its 0_Application folder, you can find a .NET 5 API developed using C# and a front-end application based on React.js.

.
├── 0. Application
│ ├── Training.API
│ │ ├── Controllers
│ ├── Training.Models
│ │ ├── Managers
│ │ ├── Store
│ └── Training.Web
│ ├── public
│ └── src
│ ├── components
│ ├── images
│ └── services
├── 1. Node.js and Docker
│ └── template
├── 2. k6 and testing
│ ├── Training.LoadTest
│ └── Training.Pipelines
└── 3. Unit testing
├── Training.Cypress
│ ├── fixtures
│ ├── integration
│ ├── plugins
│ └── support
└── Training.MSTest

Writing tests

First, you need to create the Cypress folder structure in your project.

.
└── cypress
└── integration

Inside the integration, the folder creates a new JavaScript file called products and paste the following code.

describe('Testing Product CRUD operations', () => {
beforeEach(() => {
cy.visit('http://localhost:3000/products')
})
it('Adds a new product', () => {
// Browse to add product
cy.get('nav a').eq(1).click()
cy.get('.dropdown-menu.show a').eq(1).click()
// Add product
cy.get('input[name="name"]').type('Test 01')
cy.get('input[name="price"]').type('10')
cy.get('button[type=submit]').click()
// Check that there are at least one element
cy.get('nav a').eq(1).click()
cy.get('.dropdown-menu.show a').eq(0).click()
cy.get('table[data-element-id="products"] tbody tr').should('have.length', 1)
})
it('Delete a product', () => {
cy.get('nav a').eq(1).click()
cy.get('.dropdown-menu.show a').eq(0).click()
cy.get('table[data-element-id="products"] tbody tr button').eq(0).contains('Delete').click()
cy.get('table[data-element-id="products"] tbody tr').should('have.length', 0)
})
})

This integration test is composed of two steps. Before starting the execution of each test, the test runner will browse to the URL http://localhost:3000/products. Then it will sequentially execute the listed commands. In the first step, it browses the add product page by clicking on the relative link on the navbar, then it will fill the input, and finally will submit the form. The test will finish with the assertion that the products table will contain exactly one element. Instead, the second step will simply click on the first button that has Delete as text and check that the list is empty.

Cypress test runner execution

Integration with Azure DevOps

It is time to create your build pipeline.

  • From the dashboard, select Pipelines.
  • Click the New pipeline button.
Create build pipeline
  • Select GitHub and the repository where the source code resides.
  • First, instruct the service to trigger the pipeline when there are changes in the path Training/0_Application/Training.Web of the main branch.
trigger:
branches:
include:
- main
paths:
include:
- Training/0_Application/Training.Web/*
variables:
- name: projectPath
value: './0_Application/Training.Web/'
  • Install NodeJS and the dependencies needed to run the application.
- task: NodeTool@0
displayName: 'Use Node 12.x'
inputs:
versionSpec: 12.x
- script: |
yarn install --verbose
workingDirectory: ${{variables.projectPath}}
displayName: 'Install project dependencies'

- script: |
yarn build
workingDirectory: ${{variables.projectPath}}
displayName: 'Build application'
  • Then, run the process in the background by using the command (Yarn run start&).
- script: |
(yarn run start&)
workingDirectory: ${{variables.projectPath}}
displayName: 'Start application'
  • Finally, run the integration tests and publish the results.
- script: |
./node_modules/.bin/cypress run --browser chrome
workingDirectory: ${{variables.projectPath}}
displayName: 'Run cypress tests'
- task: PublishTestResults@2
displayName: 'Publish test results'
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '*.xml'
searchFolder: '$(System.DefaultWorkingDirectory)/cypress/reports/junit'
mergeTestResults: true
testRunTitle: 'Cypress tests'

The final YAML script will look similar to this:

trigger:
branches:
include:
- main
paths:
include:
- Training/0_Application/Training.Web/*
variables:
- name: projectPath
value: './0_Application/Training.Web/'
steps:
- task: NodeTool@0
displayName: 'Use Node 12.x'
inputs:
versionSpec: 12.x
- script: |
yarn install --verbose
workingDirectory: ${{variables.projectPath}}
displayName: 'Install project dependencies'

- script: |
yarn build
workingDirectory: ${{variables.projectPath}}
displayName: 'Build application'
- script: |
(yarn run start&)
workingDirectory: ${{variables.projectPath}}
displayName: 'Start application'
- script: |
./node_modules/.bin/cypress run --browser chrome
workingDirectory: ${{variables.projectPath}}
displayName: 'Run cypress tests'
continueOnError: true
- task: PublishTestResults@2
displayName: 'Publish test results'
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '*.xml'
searchFolder: '$(System.DefaultWorkingDirectory)/cypress/reports/junit'
mergeTestResults: true
testRunTitle: 'Cypress tests'

It’s now time to push your change to your repository and run the pipeline.

Execution using Azure DevOps

Integration with GitHub Actions

First, you need to create a new GitHub Action in your repository.

  • Browse to GitHub, log in with your account and select your repository.
  • From the navigation bar, select Actions.
  • Click the New Workflow button.
GitHub Actions
  • GitHub will then propose many templates, but in our case, we need to start from scratch. For this reason, click the link set up a workflow yourself.
Setup a workflow from scratch
  • First, you need to specify what will trigger your action. In this case, I want to trigger the action every time there is a change in the directory 0_Application/Training.Web/ of the main branch.
on:
push:
paths:
- 0_Application/Training.Web/**
branches:
- main
  • To make the workflow easier to manage, I will set an environment variable with the location of my application.
env:
WORKING_DIRECTORY: './0_Application/Training.Web/'
  • It’s now time to create the core of your workflow, the jobs that the agent will execute. Because the cypress task only runs on Linux, you need to instruct GitHub to use an agent installed on an Ubuntu machine.
jobs:
build:
runs-on: ubuntu-latest
  • Then, you can move forward by checking out your source code and installing Node.js and all of the dependencies specified in your package.json.
- uses: actions/checkout@v2

- name: Set up Node.js version
uses: actions/setup-node@v1
with:
node-version: '14.x'

- name: yarn install, build, and test
run: |
yarn install
yarn build
working-directory: ${{ env.WORKING_DIRECTORY }}
  • It’s now time to perform some testing. To do so, I’m going to use different cypress-io/github-action@v2 actions for each browser.

Important: Because my application’s package.json is in a subfolder of the repository, I needed to add the parameter working-directory in each of my cypress-io/github-action@v2 task.

- name: Cypress run on chrome
uses: cypress-io/github-action@v2
with:
browser: chrome
start: yarn start
wait-on: 'http://localhost:3000'
working-directory: ${{ env.WORKING_DIRECTORY }}

- name: Cypress run on firefox
uses: cypress-io/github-action@v2
with:
browser: firefox
start: yarn start
wait-on: 'http://localhost:3000'
working-directory: ${{ env.WORKING_DIRECTORY }}

- name: Cypress run on edge
uses: cypress-io/github-action@v2
with:
browser: edge
start: yarn start
wait-on: 'http://localhost:3000'
working-directory: ${{ env.WORKING_DIRECTORY }}

The final YAML script will look similar to this:

name: Cypress testing
on:
push:
paths:
- 0_Application/Training.Web/**
branches:
- main
env:
WORKING_DIRECTORY: './0_Application/Training.Web/'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2

- name: Set up Node.js version
uses: actions/setup-node@v1
with:
node-version: '14.x'

- name: yarn install, build, and test
run: |
yarn install
yarn build
working-directory: ${{ env.WORKING_DIRECTORY }}

- name: Cypress run on chrome
uses: cypress-io/github-action@v2
with:
browser: chrome
start: yarn start
wait-on: 'http://localhost:3000'
working-directory: ${{ env.WORKING_DIRECTORY }}

- name: Cypress run on firefox
uses: cypress-io/github-action@v2
with:
browser: firefox
start: yarn start
wait-on: 'http://localhost:3000'
working-directory: ${{ env.WORKING_DIRECTORY }}

- name: Cypress run on edge
uses: cypress-io/github-action@v2
with:
browser: edge
start: yarn start
wait-on: 'http://localhost:3000'
working-directory: ${{ env.WORKING_DIRECTORY }}

It’s now time to push your change to your repository and start the workflow.

Execution using GitHub Action

After completing each step of your test, you will be able to see the execution time (in case it succeeds), the exception (in case it fails), and a summary at the end of both of the tests and GitHub Action.

Test succeeded
Test failed
Summary at the end of the GitHub Action

References: