Lecture 7: Testing (Client-side & Sever-side)
Full-Stack Development
Mark Dixon
School of Engineering, Computing and Mathematics
Last Session
• What did we do last session (write down topics, what you can
remember)?
• Did you learn anything (was anything particularly useful or good)?
• How did the lab session go (were you able to get something running)?
• What could be better?
2
Introduction
Today’s topics
1. Unit testing in JavaScript
2. Server-Side Testing
3. Server-Side Unit Testing
4. Mock Objects
5. Integration Testing
6. Website accessibility (and evaluating it)
7. Usability testing
Session learning outcomes – by the end of today’s lecture you will be able to:
• Write simple unit tests to exercise JavaScript code in the browser
• Implement unit tests for server-side JavaScript code
• Use mock objects to represent other parts of the system within tests
• Test the connections between components of a system using integration tests
• Assess the accessibility of a web page
• Conduct a usability study to test the user-acceptance of your software
3
Testing: Why test software?
“I’ll finish coding and test if I have time”
• No! Testing is an integral part of the
development process
• Can actually save you time in the long run
• Reduces bugs and improves the quality of
software
Testing considerations
• Test case design – consider edge cases, corner cases…
• Code coverage
• Black box vs white box testing
4
Testing: Types
• Unit Testing – checks individual functions/procedures within a file
• Integration Testing – checks interaction between components
• System / End-to-end Testing – checks operation of whole system
• User Testing – System may match the specification / design, but is it:
• Useful
• Usable
• Learnable
• Smoke Test – checks a few critical things (quickly)
• Penetration Testing
• Performance Testing
• ...
5
Testing: Best Practice – Automation (Tests as Code)
• automated tests can run
• every time code commited
• before deploy (& stop deploy on fail)
• supports regression testing
• changes to existing code, re-run tests (does it still work)
• unit testing is a framework for providing tests as code
• provide documentation of what the code does
• still do occasional manual testing
(in case automated tests fail, test the tests)
6
Testing: Test-driven development
1. Write the test
2. Watch it fail
• Confirm that it passes when it should
• Find bugs in the test code
• Run all tests alongside a new one
3. Make it pass
• Simplest possible implementation that causes it to pass
4. Refactor the code
7
Unit Testing: Example
[Link]
test/[Link] let express = require("express");
let chai = require("chai"); let functions = require("./functions");
let functions = require("../functions"); let app = express();
let port = 9000;
suite("Test sayHello", function() {
[Link]("/hello", function(request, response){
test("Test sayHello", function(){ [Link]([Link]());
let expected = "Hello world!!"; // Arrange. })
let actual = [Link](); // Act.
[Link](expected, actual); // Assert. [Link] = [Link](port, function () {
}) [Link]("Listening on " + port);
}) });
• To run server:
[Link]
node [Link]
function sayHello(){
• To run tests: }
return "Hello world!!";
mocha -ui tdd test/
[Link] = sayHello;
8
Unit Testing: What is a Unit?
How can we test this:
let express = require("express");
let app = express();
[Link]("/sayHello", function(request, response) {
[Link]("Hello World, from Express");
});
[Link](9000);
How can we test this:
let express = require("express");
let app = express();
function sayHello(request, response) {
[Link]("Hello World, from Express");
}
[Link]("/sayHello", sayHello);
[Link](9000);
9
Unit Testing
• Traditional Testing • Unit Testing
• whole system tested • Each part of system
tested individually
(whole system complied)
• Test scope much smaller, errors
• Errors: • detected earlier
• Easily undetected • Isolated
• difficult to track down • Faster compile time (vs whole application)
10
Unit Testing: AAA
Arrange, Act, Assert (AAA) approach
• Determine data needed (manual testing)
• try running with really simple data / "dummy data"
• can help quickly assess if unit works well
• Arrange: set up the unit to be tested
• bring system to desired state & configure (internal) dependencies
• Act: call unit (function / procedure) to be tested
• pass dependencies & capture output value (if any)
• Assert: check that actual (observed) value matches expected value
• Analyse Test results
• Failure(s) trigger debugging (cause and solution)
Mark Dixon 11
Unit Testing: Mocha and Chai
Mocha
• A JavaScript unit testing framework
• Runs in Browser (client-side) & Node (server-side)
• [Link]
Chai
• Assertion library for inclusion in JS unit tests
• Runs in Browser (client-side) & Node (server-side)
• [Link]
npm install -g mocha@10.2.0
npm install -g chai@4.5.0
npm install -g chai-http@4.4.0
12
Unit Testing: Independence
• Unit tests should be atomic
• They should not rely on side-affects of other tests
• The order in which they are executed should not matter
To make a test atomic
• Get original values from the application/web page
• Set up the values for the test
• Restore the original values after the test has run
Don’t
• Store values in a variable that other tests might change (e.g. a click counter)
13
Integration Testing: Example
test/[Link] [Link]
let chai = require("chai"); let express = require("express");
let chaiHttp = require("chai-http"); let functions = require("./functions");
let server = require("../server"); let app = express();
[Link](chaiHttp); let port = 9000;
suite("Suite routes", function() { [Link]("/hello", function(request, response){
test("Test GET /hello", function() { [Link]([Link]());
let app = [Link]; // Arrange. })
[Link](app).get("/hello") // Act.
.end(function(error, response) { [Link] = [Link](port, function () {
[Link]([Link], 200"); // Assert. [Link]("Listening on " + port);
}); });
});
});
• To run server:
[Link]
node [Link]
function sayHello(){
• To run tests: }
return "Hello world!!";
mocha -ui tdd test/
[Link] = sayHello;
14
Integration Testing: Possible failures
Possible integration failures
• Incorrect method invocation
• Methods invoked correctly but in the wrong sequence
• Timing failures – race condition
• Throughput/capacity problems
To test these, your integration tests will…
1. Read/write to a database
2. Call a web service
3. Interact with the file system
15
Integration Testing: databases
Set up a test database to run integration tests against it
1. Before any of the tests run set up a test instance of the database
2. Before each test set up test data so that tests are executed against a standard
setup
3. Execute the test
4. After each test clean the data from the database
5. At the end of the suite delete the test database
16
Integration Testing: Best practice
1. Integrate early, integrate often
2. Don’t test business logic with integration tests
3. Keep test suites separate
4. Log often (but performance tade-off)
5. Follow a test plan
6. Automate whatever you can (but still manually test occasionally)
17
Mock Objects: What & Why
Use mock objects when the real object
• Has non-deterministic behaviour
• Is difficult to set up
• Has behaviour that is hard to trigger (e.g. network error)
• Is slow
• Has (or is) a user interface
• Does not yet exist
Implementing mock objects
1. Use an interface to describe the object
2. Implement the interface for production code
3. Implement the interface in a mock object for the test
18
Mock Objects: Sinon Timers
Control the system time for time/date specific tests
1. Initialise the date/time as required
2. Run the test
3. Restore the actual date/time
let chai = require("chai");
let sinon = require("sinon");
let logic = require("./logic");
suite("Test message generator", function() {
test("Check morning message is correct", function() {
let date = new Date(2021, 11, 1, 10, 0, 0, 0);
let clock = [Link](date);
let msg = [Link]();
[Link]("Good morning", msg, "Wrong message for 10am");
[Link]();
}
}
19
Mock Objects: Sinon Spies
Inspect the calling of functions
• e.g. has a function been called?
let chai = require("chai");
let sinon = require("sinon");
let routes = require("./routes");
suite("Test Express Router", function() {
test("GET greetingRoute", function() {
let request = {};
let response = {};
[Link] = [Link]();
[Link](request, response);
[Link]([Link]);
}
}
20
Client-side Testing: Writing a unit test
1. Set up the test function stub
2. Preserve any original page properties
3. Set up the test conditions
4. Test the result of updating the conditions
5. Reset the page properties
suite("Test suite description", function() {
test("Test 1 description", function() {
// Test code goes here.
});
test("Test 2 description", function() {
// Test code goes here.
});
});
21
Client-side Testing: Writing a unit test
Preserve the original page properties
let originalCol = $(".light").css("background-color");
Set up the test conditions
$(".light").css("background-color", "#00ffff");
$("#button").trigger("click");
Test the result of updating the conditions
let lightCol = rgb2hex($(".light").css("background-color"));
[Link](lightCol, "#ffff00", "Class has wrong colour");
Reset the page properties
$(".light").css("background-color", originalCol);
Is this server-side code or client-side code?
22
Client-side Testing: Hooks
Suites allow repeated code to be suite("A suite", function() {
placed in a single place suiteSetup(function() {
// Prepare something once for all
// tests
});
Available hooks suiteTeardown(function() {
• suiteSetup – runs before the first // Clean up once after all tests.
});
test setup(function() {
• suiteTeardown – runs after the // Prepare something before each
// test.
last test
});
• setup – runs before each test teardown(function() {
// Clean up after each test.
• teardown – runs after each test });
test("A test", function {
// Test code here…
});
});
23
User Testing
There is always another app – understand your user…
• What type of experience?
• What is their professional background?
• What are their needs and interests?
• How are they currently trying to meet these needs?
• Where and when would they use this app?
24
User Testing: Customer information
Educated assumptions
• Avoid obvious misconceptions
Focus groups, interviews…
Organising customer information
• Assumption personas – description of target users
• Create the product for someone we believe exists rather than our idea of the user
• Help team members share understanding of audience groups
• Prioritise features by how well they meet user users
25
User Testing: User centred design
• Needs, wants and limitations are the focus for the design process
Five steps for UCD
1. Identify primary features based on personas
2. Understand why the persona needs the features
3. Investigate how other apps provide such features
4. Sketch/wireframe your idea
5. Test design with users
26
User Testing: Test design with users
Test with real content
Three simple questions
• What does this feature do?
• What do you like about it?
• What don’t you like about it?
Tips for usability testing
• Test the feature, not the user
• Remain neutral – listen and learn and don’t assist the user (failure is informative)
• Take good notes (audio and/or video record, but only if user consents)
• Keep testing throughout the design process
• Follow Ethics Procedure: Information Sheet -> Consent Form -> Activity -> Debrief
27
Google Lighthouse
“open-source, automated tool for improving the quality of web pages”
• Audits webpages against range of criteria
1. Performance
2. Accessibility
3. Best practices
4. SEO (Search Engine Optimisation)
5. Progressive web app
• Runs
• within browser or
• Node package from the terminal
28
Exercise 1: sayHello
• In the first exercise your task is to unit test a function. The function should be
called sayHello, and it should return a string. The string should be made of two
substrings – one is “Hello” and the other is stored in a variable called name,
which is passed to the string as an argument.
• Therefore, if the value of the name variable is “Mark”, the function should return
“Hello Mark”.
• Download the template on the DLE and modify it to include:
• A test suite containing a unit test that asserts that the function returns the correct function.
• The sayHello function, so that the test runs and passes.
You will need to use the
clickElement function, which
is included in the template for
you.
29
Exercise 2
• Your next task is to take the function developed in Exercise 1 and use it to display
the message in the page. You should get the name from a text input. Include a
button in the page with a click event handler that takes the text from the input
field, generates the message, and inserts it into the page.
• Extend the template by adding a test to the suite so that:
• A value is assigned to the text box
• The button is clicked
• The message is inserted into the page
• You will also need to implement the function so that the test passes, and should
include a teardown method to remove the values from the form and the message
element.
30
Exercise 3: Calculator
• Begin by adding the following HTML to the template.
<div id="calculator">
<p id="result">0.00</p>
<input id="number1" /><input id="number2" />
<button id="btnAdd">Add</button>
<button id="btnMuk">Mul</button>
</div>
• Then modify the CSS with the following lines.
#calculator { width: 150px; background-color: #dfdFDF; padding: 5px; }
#calculator input { width: 50px; float: left; margin: 5px; }
31
Exercise 3: Calculator
• Create a suite for the calculator tests. You’re going to need a suiteSetup method to setup
the test data you will use in the tests. Start the suite as follows:
suite("Calculator suite", function() {
suiteSetup(function() {
self.x = [1, 2, 3, 4, 5];
self.y = [2, 4, 6, 8, 10];
})
});
• Then add a test method, which:
• Defines test data (adding together the data you defined in your suiteSetup method).
• Loops over the test data and asserts that the sum function produces the correct answer for each pair of inputs.
• Once you’ve written the test, add the sum function to the application and your test should
pass.
You can still use
• Repeat the steps taken in the previous exercise to ensure that the product function operates self.x and self.y as
correctly. The function should multiply values rather than adding them. your test data.
32
Exercise 3: Calculator
• The final part is to test the user interface. Begin by adding a test that checks the
following attributes of the result paragraph:
• Is the background #ffffff?
• Is the font-size 20px?
• Then implement the CSS needed to make the tests pass.
• The next step is to check that the buttons work correctly. Beginning with the sum button,
add a test method that:
• Adds values to the two inputs Again, use clickElement
• Clicks the button here.
• Confirms that the correct result is shown in the page
• To get the test to pass, implement the event handler that will populate the result with
the correct value.
• When the sum button is working and tested, repeat the process for the multiply button.
• Finally, add a teardown method that will remove the values from the inputs and results
paragraph when each test has run. This is similar to the
suiteSetup method above –
look at the lecture slides to
check the syntax.
33
Exercise 4: Hello World Server
• A previous session required you to implement the hello world server discussed in that week’s
lecture. It contains an anonymous function within a route that sends a hello world message to the
client. The example Unit Test in this session is very similar.
• Refactor your answer to that exercise so that the route calls a named function, and
• add a unit test to make sure it works correctly.
• Write an integration test for the Hello World (Exercise 1) activity you created using
[Link] (in an earlier session). The example Integration Test in this session is very
similar. Your test should connect to the Express server you built, access the /hello
route and confirm that it returns:
• The correct status code
• The correct text
34
Exercise 5: Roll
• Write a set of tests that confirms that the roll function (that you created in Node [Link] in a earlier
session) operates correctly. You should begin by writing a unit test that confirms that an integer from the
set {1, 2, 3, 4, 5, 6} is returned when the function is called.
• Create a route in the workshop app that exposes the roll function and returns the number thrown
(without the template that you used in that Workshop – we’re just after capturing the number thrown by
the dice) as follows:
let express = require("express");
let path = require("path");
let functions = require("./functions");
let port = 9000;
app = express();
[Link]("/roll", function(request, response) {
let number = [Link]();
[Link]([Link]());
});
[Link](port, function() {
[Link]("Listening on " + port);
});
• Write an integration test to confirm that when that route is called a valid result is returned.
35
Exercise 6: Good Morning / Afternoon / Evening
• Write a function that returns an appropriate greeting depending on the time of day.
Your code should therefore check the current hour from the system time.
• Use a fake timer from Sinon to unit test the function and ensure it works correctly.
(the first test is provided earlier in this session)
36
Exercise 7: Extension tasks (optional)
• Extend the calculator exercise by adding in other common mathematical
operations – you could include subtraction, division, raising to a power and
rooting numbers, for example. The process will be the same as for the operations
included earlier, but the process of writing the tests is good practice. Try to think
of a more extensive range of tests that you could include.
• By this point you have spent several weeks writing server-side code. You should
design and build test suites for some of this code. Try to think carefully about the
test cases and plan the most exhaustive set of tests you can. How much of the
code can you cover with tests?
37