Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial version of a e2e test suite created with Cypress and Cucumber #145

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ testem.log
# e2e
/e2e/*.js
/e2e/*.map
/e2e/cypress/screenshots
/e2e/cypress/videos
/e2e/cypress/reports

# System Files
.DS_Store
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,9 +164,19 @@ Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.

## Running end-to-end tests

### Protractor (deprecated)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I propose to remove Protactor and keep the e2e goal that is common.

Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
Before running the tests make sure you are serving the app via `ng serve`.

### Cypress

Run `npm full-regression-test` to run the end-to-end test via [Cypress](http://www.cypress.io/).
The app will be started by the test script, but you need to make sure, that the back end rest server is running.

For further information see the [test documentation](e2e/cypress/docs/README.md).



## Further help

To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
3 changes: 3 additions & 0 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -151,5 +151,8 @@
"@angular-eslint/schematics:library": {
"setParserOptionsProject": true
}
},
"cli": {
"analytics": false
}
}
30 changes: 30 additions & 0 deletions cypress.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { defineConfig } from "cypress";
import * as createBundler from "@bahmutov/cypress-esbuild-preprocessor";
import { addCucumberPreprocessorPlugin } from "@badeball/cypress-cucumber-preprocessor";
import createEsbuildPlugin from "@badeball/cypress-cucumber-preprocessor/esbuild";
export default defineConfig({
e2e: {
specPattern: "e2e/features/**/*.feature",
supportFile: 'e2e/cypress/support/**/e2e.{js,jsx,ts,tsx}', // Adjust this if you have a support file
fixturesFolder: 'e2e/cypress/fixtures',
screenshotsFolder: 'e2e/cypress/screenshots',
videosFolder: 'e2e/cypress/videos',
downloadsFolder: 'e2e/cypress/downloads',
video: false,
async setupNodeEvents(
on: Cypress.PluginEvents,
config: Cypress.PluginConfigOptions
): Promise<Cypress.PluginConfigOptions> {
// This is required for the preprocessor to be able to generate JSON reports after each run, and more,
await addCucumberPreprocessorPlugin(on, config);
on(
"file:preprocessor",
createBundler({
plugins: [createEsbuildPlugin(config)],
})
);
// Make sure to return the config object as it might have been modified by the plugin.
return config;
},
},
});
82 changes: 82 additions & 0 deletions e2e/cypress/docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# E2E testing with Cypress and Cucumber

## Overview

The implemented e2e test suite uses [Gherkin](https://cucumber.io/docs/gherkin/reference/) feature files for test descriptions. The test system reads these feature files and links each scenario test step to a step definition in a JavaScript file. In the step definition, we implement the corresponding test actions by calling page object functions. These page objects realize specific test functions of the application under test using [Cypress](http://www.cypress.io/).


## Test suite structure


### cypress.config.ts

The file cypress.config.ts is stored under the project root.
This file contains the basic configuration for the test suite. The most crucial part includes the definitions of the locations, where the different test files (input or results) are stored.

### Feature files

We store the feature files under ***e2e/features***.
The [Gherkin](https://cucumber.io/docs/gherkin/reference/) language is used to write test system agnostic descriptions of test cases.
#### Special Keywords
In this test suite, we use the following special keywords in test steps or example tables.

EMPTY
SPACES

EMPTY is automatically replaced during testing by an empty string: "".
SPACES is automatically replaced by a string comprising only space characters: " ".


#### Tags

There are several tags defined in the test suite.

Important is the tag `@Bug`. This tag shows scenarios that will fail because of a known bug. With `npm confirmation-test` one can retest these exclusively during fixing bugs.

### Step definitions
You can find the step definition files under ***e2e/cypress/support/step_definitions***.
Developers write step definition files in JavaScript or (in the future) TypeScript.
Besides the definition of the keywords (Given, When, Then, And) these files are test suite agnostic. The test suite needs to be written in Javascript or TypeScript.

It is important to make sure that the string value of a step definition header fits to the scenario steps in the feature files.

### Page objects

You can find the page object files under ***e2e/cypress/support/page_objects***.

They contain page classes that implement the test tool specific automation code. Each function implements one input or verification sequence of a single page of the application under test.

### Tools

You can find the tool files under ***e2e/cypress/support/tools***.
Currently, the only tool is the implementation for a rest api access to delete a specific pet owner. The reason for this is that there is no UI function in the Angular application under test to delete a pet owner.

## Running the test suite

There are different ways to call the test suite.

### Interactive with the Cypress app

In the terminal enter `npx cypress open`.

Then the cypress app will open. Select E2E testing and electron browser to test. You now will see a list of feature files to run interactively.


### Headless with NPM

There are two test execution variants defined in the package.json

`npm full-regression-test`- this will run all feature files
`npm confirmation-test`- this will run only scenarios or features tagged with @Bug.

## Reports

After a test run there is an [HTML report](../reports/html/cucumber-report.html) available under ***e2e/cypress/reports/cucumber-report.html***.

You can find the screen shot files under ***e2e/cypress/screenshots***.

## FAQ

### I want to run the rest server backend or the Angular front end from a different base URL than in the example defined.

The test suite uses the file ***e2e/cypress/fixtures/config.json*** to define the base URLs of the rest server app and the Angular front end app. If these URLs are not the standard values, these need to be changed.
4 changes: 4 additions & 0 deletions e2e/cypress/fixtures/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"baseURL":"http://localhost:4200/petclinic/",
"restURL":"http://localhost:9966/petclinic/api/"
}
49 changes: 49 additions & 0 deletions e2e/cypress/support/commands.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/// <reference types="cypress" />
// ***********************************************
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
//
// declare global {
// namespace Cypress {
// interface Chainable {
// login(email: string, password: string): Chainable<void>
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
// }
// }
// }

Cypress.Commands.add('typeAndVerify',(path,value) => {

if (value.trim() == "EMPTY") {
cy.get(path).type('{selectall}{backspace}{selectall}{backspace}');
} else if (value.trim() == "SPACES") {
cy.get(path).type('{selectall}{backspace}{selectall}{backspace}').type(" ");
}else {
cy.get(path).clear();
cy.get(path).type('{selectall}{backspace}{selectall}{backspace}').type(value).should('have.value', value);
}
})
20 changes: 20 additions & 0 deletions e2e/cypress/support/e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// ***********************************************************
// This example support/e2e.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************

// Import commands.js using ES2015 syntax:
import './commands'

// Alternatively you can use CommonJS syntax:
// require('./commands')
19 changes: 19 additions & 0 deletions e2e/cypress/support/page_objects/BaseClass_PO.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/// <reference types="cypress" />

class BaseClass_PO {

constructor() {
this.selectors = new Map(); // Initialize the Map, but content will be added in child classes
}

addSelector(name,value){
this.selectors.set(name,value);
}
selector(name){
return this.selectors.get(name);
}


}

export default BaseClass_PO;
79 changes: 79 additions & 0 deletions e2e/cypress/support/page_objects/BaseInputList_PO.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
///<reference types="cypress" />

class BaseInputList_PO {


navigateHome() {
cy.get('button.btn').contains('Home').click();
}

validateNotInList(name){
cy.get('table tbody tr').each(($row, index, $rows) => {
cy.wrap($row).find('input').then($input => {
cy.wrap($input).should('not.have.value', name);
});
});
}

validateInList(name){

let textFound = false;

cy.get('table input').each(($input) => {
if ($input.val() === name) {
textFound = true;
}
}).then(() => {
// Assert that the text was found in at least one input field
expect(textFound).to.be.true;
});
}

validateInitialArray(dataArray) {

cy.get('table tbody tr').each(($row, index, $rows) => {
cy.wrap($row).find('input').then($input => {

cy.wrap($input).should('have.value', dataArray[index]);
});
});
}


deleteListElement(name) {

cy.get('table tbody tr').each(($row, index, $rows) => {

cy.wrap($row).as('currentRow');

cy.get('@currentRow').find('input').invoke('val').then((inputValue) => {
if (inputValue == name) {

cy.wrap($row).contains('Delete') .should('exist').as('deleteButton');

}
});
});

cy.get('@deleteButton').click();

}



beginAdd(){
cy.get('button.btn').contains('Add').click();
}

clearElement() {
cy.get('#name').clear();
// cy.get("#name").type('{selectall}{backspace}{selectall}{backspace}');
}

setName(name){
cy.typeAndVerify("#name", name);
}
}


export default BaseInputList_PO;
Loading