Lecture 7: Testing (Client-side & Server-side) - University of Plymouth
Document Details
Uploaded by Deleted User
University of Plymouth
Mark Dixon
Tags
Summary
Lecture notes on testing, client-side and server-side, for a full-stack development course at the University of Plymouth. The lecture covers unit testing, integration testing, and related topics.
Full Transcript
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 a...
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 server.js test/unit-tests.js 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() { app.get("/hello", function(request, response){ test("Test sayHello", function(){ response.send(functions.sayHello()); let expected = "Hello world!!"; // Arrange. }) let actual = functions.sayHello(); // Act. chai.assert.equal(expected, actual); // Assert. app.server = app.listen(port, function () { }) console.log("Listening on " + port); }) }); To run server: functions.js node server.js function sayHello(){ To run tests: } return "Hello world!!"; mocha -ui tdd test/ module.exports.sayHello = sayHello; 8 Unit Testing: What is a Unit? How can we test this: let express = require("express"); let app = express(); app.get("/sayHello", function(request, response) { response.send("Hello World, from Express"); }); app.listen(9000); How can we test this: let express = require("express"); let app = express(); function sayHello(request, response) { response.send("Hello World, from Express"); } app.get("/sayHello", sayHello); app.listen(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) https://mochajs.org/ Chai Assertion library for inclusion in JS unit tests Runs in Browser (client-side) & Node (server-side) https://www.chaijs.com/ npm install -g [email protected] npm install -g [email protected] npm install -g [email protected] 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/integration-tests.js server.js let chai = require("chai"); let express = require("express"); let chaiHttp = require("chai-http"); let functions = require("./functions"); let server = require("../server"); let app = express(); chai.use(chaiHttp); let port = 9000; suite("Suite routes", function() { app.get("/hello", function(request, response){ test("Test GET /hello", function() { response.send(functions.sayHello()); let app = server.app; // Arrange. }) chai.request(app).get("/hello") // Act..end(function(error, response) { app.server = app.listen(port, function () { chai.assert.equal(response.status, 200"); // Assert. console.log("Listening on " + port); }); }); }); }); To run server: functions.js node server.js function sayHello(){ To run tests: } return "Hello world!!"; mocha -ui tdd test/ module.exports.sayHello = 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 = sinon.useFakeTimers(date); let msg = logic.greetingMessage(); chai.assert.equal("Good morning", msg, "Wrong message for 10am"); clock.restore(); } } 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 = {}; response.send = sinon.spy(); routes.greetingRoute(request, response); chai.assert.isTrue(response.send.calledOnce); } } 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")); chai.assert.equal(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. 0.00 Add Mul 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 Express.js (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 Express.js 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(); app.get("/roll", function(request, response) { let number = functions.roll(); response.send(number.toString()); }); app.listen(port, function() { console.log("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