NSWI153: Advanced Web Applications

Software testing

Preliminaries

Please read this section at least one day prior to the seminar. This section outlines what you are expected to know, be able to do, or prepare in advance. Following these instructions will help you get the most out of the seminar and ensure smooth participation.

Remote

This is a remote practical. There is no on-site activity. Instead, you are expected to solve the assignments in the time and place of your choosing. Unless stated otherwise, you are expected to work alone. You are not allowed to share your solution with your colleagues. Each assignment has a specific deadline and submission conditions.

Rules of this practical

You are not allowed to collaborate with your colleagues.

Avoid over-reliance on AI tools for implementing this assignment.

You are required to finish all exercises, commit and push your solution, and deploy both backend and frontend before 2026-05-12 23:59 (UTC+1). Make sure to include the deployed URLs in your repository README.

This practical is designed as a series of exercises that build on each other. You are expected to solve the exercises in the given order and carry your source code forward. It is a good idea to commit after each exercise.

Exercise: Vitest

How do you know your software works?

Assignment: Testing with Vitest

./practical-06/


Vitest is a test framework with Vite-native support. Meaning, you can easily use it with Vite project.

The objective is to implement Vitest for unit testing.

Unit testing is the process where you test the smallest functional unit of code.

Vitest has an excellent Getting Started. The main points to follow are:

  • Install Vitest and save it to dev-dependencies.
    
              npm install -D vitest
            
  • Use .spec. in test file name. Place test files next to your source files. Do NOT use separate directory for test files.
  • Add "test":"vitest" command to script section in "package.json".
  • Do NOT put configuration into "vitest.config.ts" use "vite.config.ts" instead.

Continue to the next slide once you are done >>>

Example test


      // Vitest provides:
      // - test executor : the vitest command
      // - test definition : describe, test
      // - test assertions : expect
      import { describe, test, expect } from "vitest";
      // We import what we aim to test.
      // This should be only a single file for a unit tests.
      import { createState } from "./application-state";
      // We define a collection of test relevant for given function.
      describe("createState", () => {
        // This is the single test.
        // The test function can be asynchronous (async).
        test("Default test.", () => {
          // Prepare input and initial state.
          const search = "?data-source=1&submit-url=2";
          // Compute actual state.
          const actual = createState(search);
          // Define and assert for expected state.
          const expected = { dataSource: "1", submitUrl: "2", initialized: false, submitted: false };
          // We use "toStrictEqual" to check for the internals of the object.
          expect(actual).toStrictEqual(expected);
        });
      });
    

It is easier to write tests if you have properly utilized decompositions.

Continue to the next slide once you are done >>>

Unit test

Your objective is to implement unit tests into your JavaScript user interface application. Work with the code from assignment 06. You are allowed to modify the code, but try to keep the changes to minimum.

You need to implement at least three tests to satisfy following:

  • A simple synchronous test for business functionality.
  • An asynchronous test testing an async function.
  • Create test using a mock.

Continue to the next slide once you are done >>>

Test coverage and Vitest

The next step is to add a test coverage. This provides us with an estimate of how much code we test. Beware that test coverage should not be the main metric! Yet, when paired with additional rules it can be useful. E.g. 100% test coverage for controllers.

Computing test coverage with Vitest is easy. Output the tests as JSON and HTML using reporters. You can configure the reporters using "vite.config.ts" file.

Add a "coverage": "vitest run --coverage" command to "package.json" to run the tests and compute the coverage.

Continue to the next slide once you are done >>>

Do not ignore .gitignore

By default the coverage report is saved to "coverage" directory. As a result, you need to add "coverage" into your ".gitignore" file.

Congratulations, if you have followed the instructions, you have just reached the end of this exercise.

Exercise: CI using GitLab

Run the tests automatically on every push.

Assignment: Continuous Integration (CI)

./.gitlab-ci.yml


The objective is to implement a simple CI pipeline using GitLab. You should already know the basics from NSWI177.

  • You need to create `.gitlab-ci.yml` file in root of your repository.
  • Take a look at the "NodeJS" template pipeline under "Build" -> "Pipelines".
    Note: This is available only when there is no pipeline in the repository.
  • Start with node image of your choosing. Do NOT use "latest", we need as much determinism as we can get.
  • Define "build" job that will build your application using "npm run build".
  • Define "test" job that will test you application using "npm run test".

You can read the .gitlab-ci.yml documentation.

Exercise: End-to-end test and Cypress

We would like to interact with our application the same way as the user.

End-to-end (E2E)

End-to-end (E2E) testing is a Software testing methodology to test a functional and data application flow consisting of several sub-systems working together from start to end.
End-to-end testing, also known as E2E testing, is an approach to testing that that simulates real user experiences to validate the complete system.

For the purpose of this practical we limit ourself only to simulating a user interaction with the dialog. We basically simulate user interaction with our application using a web browser.

Continue to the next slide once you are done >>>

A browser

In order to implement the tests we need to:

  1. Have our application running.
    We can run our application using "npm run dev" or "npm run preview".
  2. Start a web browser
  3. Control the web browser from code

When it comes to the browser and its control, we can use Playwright, Puppeteer, Cypress, ...


For purpose of this practical we utilize Cypress as it has simple setup, intuitive API, built in assertions, automatic waiting, ...

Continue to the next slide once you are done >>>

Cypress install

./practical-06/


There is a Cypress Install guide. Please install Cypress using npm "npm install cypress --save-dev".

Cypress integrates a custom application which allow you to easily create and execute the test. Before opening the application you may need to modify your "tsconfig.json" and add following fragment.


      "compilerOptions": {
        "module": "ESNext"
      },
    

Continue to the next slide once you are done >>>

First test

Next open the application using "npx cypress open" and select "E2E testing". Confirm quick configuration and select a browser you would like to choose. From the perspective of the tutorial you should continue with Your First Test.

Select "Create new spec" and accept the default.


      describe('template spec', () => {
        it('passes', () => {
          cy.visit('https://example.cypress.io')
        })
      })
    

Look around the application. Try to modify the source file, by default "spec.cy.js" using your IDE of choice. Reload and re-execute the test.

Continue to the next slide once you are done >>>

Testing your application

In order to run the test for our application we need to start it first. Start your application using "npm run dev" and update the test in "spec.cy.js" to load your application. Do not point Cypress to webik, instead point it to your local dev server.

You may get a "ECONNREFUSED" error. This meas that the browser is not able to access your application. You can test it manually by opening a new tab and navigating to URL of your application. Try to solve this on your own using any tools necessary. There is also a possible solution in this presentation source.

Continue to the next slide once you are done >>>

Selected API 1 / 2

Cypress provide a rich API. While I do recommend you to take a explore the official documentation, here is a list of few of the basic one:

  • cy.visit - Visit a given URL.
  • cy.contains - Get DOM element with given text.
  • cy.intercept - Allow you to spy and mock network communication. You can mock (stup) a HTTP request. Meaning you can intercept the call and return custom response.
  • cy.wait - Wait for given time or aliased resource.

Continue to the next slide once you are done >>>

Selected API 2 / 2

Here is a simple example using API from the previous slide.


      // Intercept POST call to "http://example.com/data-submit", return "{}" as a response, and assign "postSubmitData" as an alias.
      cy.intercept("POST", "http://example.com/data-submit", "{}")
        .as("postSubmitData");

      // Select by text and perform click operation.
      cy.contains("Send").click();

      // Wait for application to make the POST request and check the content.
      cy.wait("@postSubmitData").then(interception => {
        const request = JSON.parse(interception.request.body);
        expect(request.title).to.be.equal("Instance");
      });
    

Continue to the next slide once you are done >>>

Cypress fixture

"fixture" provide a way how to fetch data content from files in "./cypress/fixtures". You can pair it with intercept like this:


      cy.intercept("GET", "data-source", { fixture: "data-source" })
        .as("getDataSource");
    

This will load file "./cypress/fixtures/data-source.json".

Continue to the next slide once you are done >>>

Test F01) A user can list all libraries

  • Start by visiting your application page to list all libraries.
  • Mock the GET request to return predefined JSON content using Cypress "fixture". The JSON must contain entries for at least for two libraries.
  • Make sure all the libraries from the JSON file are rendered. You can hard-code the check.

Continue to the next slide once you are done >>>

Test F02) A user can create a new library.

  • Start by visiting your application to page to create a new library.
  • Simulate user action via the browser to fill in all necessary fields and confirm the creation.
  • Intercept the POST request and validate the content of the request.

Continue to the next slide once you are done >>>

Configuration and .env

Update your cypress test to test application running at URL as defined by APPLICATION_URL. Value of APPLICATION_URL can be specified as environment variable or using APPLICATION_URL property in .env file.

You can utilize cy.env to access environment variables from tests. You can load .env file to Cypress using following configuration file:


      # Import package and load all variables .env into process.env object.
      import dotenv from "dotenv";
      dotenv.config();

      import { defineConfig } from "cypress";

      export default defineConfig({
        env: { APPLICATION_URL: process.env.APPLICATION_URL }
      });
    

Do not forget to install and save "dotenv" as a dependency.

Continue to the next slide once you are done >>>

Update .gitignore and package.json

  • Remember to update .gitignore file.
  • Add "cy:run": "cypress run" to your "package.json" file.

Congratulations, if you have followed the instructions, you have just reached the end of this exercise.

Questions, ideas, or any other feedback?

Please feel free to use the anonymous feedback form.