knowledge-kitchen
/
course-notes
class: center, middle # Unit Testing Validating the behavior of functions. --- # Agenda 1. [Overview](#overview) 2. [Example](#example) 3. [Advantages](#advantages) 4. [Test Cases](#test-cases) 5. [Assertions](#assertions) 6. [Best Practices](#best-practices) 7. [Doubles](#doubles) 8. [Testing Tools](#tools) 9. [Conclusions](#conclusions) --- name: overview # Overview -- ## Concept A **unit** is the smallest testable unit of code - typically a function, but sometimes an object, a specific property of an object, or a class. -- - Unit testing verifies that each unit behaves as expected, in every situation where it might find itself. --- template: overview ## Example For example, given a function that _accepts two integer arguments_ and _returns their sum_, unit tests might verify that the function returns the correct sum in all cases, such as when given... -- - two positive integers -- - two negative integers -- - a positive and a negative integer -- - a negative and a positive integer -- - zero and a positive integer -- - a positive integer and zero -- - zero and a negative integer -- - a negative integer and zero -- - zero and zero -- - arguments that are not integers -- - etc. --- template overview ## Scale As you can see, even a simple function requires many unit tests in order to be thoroughly tested. -- - In practice, there is often more _test code_ than _production code_. -- - For this reason, it is imperative to write test code as _seriously_ as you would write production code. --- template: overview ## Background Unit testing is an automated testing technique developed in 1992 by Kent Beck to solve the annoyance of manually checking code for obvious errors. -- - the first testing framework was written in Smalltalk for testing Smalltalk code -- - Beck ported this concept to Java, creating the **JUnit** testing framework, as a way to teach himself Java during the an airplane flight from Zurich to Upsalla in 1997 -- - Beck shared JUnit with Martin Fowler, a fellow signatory of the [Agile Manifesto](/content/courses/agile-development-and-devops/slides/scrum/#agile-manifesto), who helped spread it further in the developer community -- - now the concept has been ported to just about every high-level programming language --- template: overview ## Other languages The world of unit testing frameworks for different languages that are ultimately based on JUnit is called **xUnit**. -- - all open source, since JUnit was released open source -- - all written in the same language that they are testing -- - all based on the concept of **assertions**, statements about what is believed to be true in the code that are then tested against reality, e.g. `assertEquals(12, 3*4)`. --- name: advantages # Advantages -- ## Comparison to Alternative Automated unit testing has many advantages over manual debugging: -- - less time consuming and tedious -- - requires fewer human resources -- - provides immediate feedback of failures -- - allows developers to focus more on writing code -- - more reliable than manual testing --- template: advantages ## Relationship to Refactoring **Refactoring** of existing code involves rewriting it to make it better organized and efficient without changing its observable behavior. -- - It is often necessary to refactor code to make it more easily tested with unit tests - keep each function short and focused on a single task. -- - With automated unit testing, refactoring will not accidentally introduce errors, since any such errors that change the behavior of the code will be immediately detected by the tests (called regression tests in this context). -- > In almost all cases, I’m opposed to setting aside time for refactoring." > > -[Martin Fowler](https://martinfowler.com/), author of 'Refactoring', the book --- name: examples # Examples -- ## Java A unit test example in Java using JUnit: -- ```java // include JUnit and assertion libraray import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; public class MyFirstTest { @Test public void multiplicationOfZeroIntegersShouldReturnZero() { MyClass tester = new MyClass(); // MyClass is tested // assert statements for a variety of test cases assertEquals(0, tester.multiply(10, 0), "10 x 0 must be 0"); assertEquals(0, tester.multiply(0, 10), "0 x 10 must be 0"); assertEquals(0, tester.multiply(0, 0), "0 x 0 must be 0"); } } ``` --- template: examples ## Javascript A unit test example in Javascript using `mocha`: -- ```js // use mocha's built-in assertion library const assert = require("assert") // a set of tests of array functions describe("Array", function () { // one particular unit test describe("#indexOf()", function () { // assert what should be returned it("should return -1 when the value is not present", function () { // test that assertion assert.equal(-1, [1, 2, 3].indexOf(4)) }) }) }) ``` -- If the project's `package.json` file is set up with a script called `test` that contains the command to execute the tests using, then the tests can be run from the same directory as the `package.json` file with `npm test`. --- template: examples ## Javascript (continued) A unit test example of a back-end route using `mocha`, `chai`, and the [chai-http](https://www.chaijs.com/plugins/chai-http/) extension for simple testing of routes. -- ```js describe("GET request to /foo route", () => { it("it should respond with an HTTP 200 status code and an object in the response body", done => { chai .request(server) .get("/foo") .end((err, res) => { res.should.have.status(200) // use should to make BDD-style assertions res.body.should.be.a("object") // our route sends back an object res.body.should.have.property("success", true) // a way to check the exact value of a property of the response object done() // resolve the Promise that these tests create so mocha can move on }) }) }) ``` --- template: examples ## Python A unit test example in Python using `pytest`: -- ```python import pytest from some_package import some_module class Tests: def test_some_function(self): ''' Verify some_function() returns a non-empty string. ''' actual = some_module.some_function() # get the actual return value of the function assert isinstance(actual, str), f"Expected some_function() to return a string. Instead, it returned {actual}" assert len(actual) > 0, f"Expected some_function() not to be empty. Instead, it returned a string with {len(actual)} characters" ``` -- Note that `pytest` works best when all test files are in a `tests` directory with filenames that start with `test_` and all test functions start with `test_`. - just run the command `python -m pytest` from the main project directory to run all tests. Try '`python3`' instead of '`python`' if your system requires it. --- template: examples ## Python (continued) A unit test example of a back-end route using the [pytest-flask](https://pytest-flask.readthedocs.io/en/latest/index.html) extension for simple testing of routes. -- ```python import pytest from project import create_app class Tests: def test_foo(self): app = create_app() # this method must be defined in production code and must return the flask app object with app.test_client() as test_client: response = test_client.get('/foo') response_data = response.data.decode('utf8') # get plain text response data assert response.status_code == 200 "/foo should respond with an HTTP 200 status code" # if the response data is expected to be in JSON format response_obj = json.loads(response_data) # convert json response body data into an object assert response_obj.success == True, "Expected the response JSON to have a property 'success' with value True" # ... or if the response data is expected to be HTML format assert "
foobar
" in response_data, "Expected the response HTML to have a paragraph with 'foobar' in it" ``` --- name: test-cases # Test Cases -- ## Minimal Number Unit tests must test at least two scenarios, called **test cases**: -- - **Valid input** - test the expected output when given valid input (e.g. an integer within the expected range) -- - **Invalid input** - test the expected output when given invalid input (e.g. a string when an integer is expected, and/or input of the correct type but out of the expected range) -- In actuality, there will usually be **many test cases** per production function for every possible variety of valid and invalid input. --- name: assertions # Assertions -- ## Concept Every unit testing framework depends upon a set of **assertions** - purported truths that are then compared against reality. -- - An assertion is a statement that a particular predicate is going to be true - this is either true or false. -- - If false, the assertion fails and throws an error about what went wrong and where that happened in code --- template: assertions name: assertions-1 ## Typical Assertions This is a partial list of methods in **JUnit's asssertion library** that all other testing frameworks tend to imitate: -- ### Equality: - `assertEquals(primitive/object expected, primitive/object actual)` - `assertNotEquals(primitive/object expected, primitive/object actual)` - `assertArrayEquals(expectedArray, actualArray)` --- template: assertions-1 ### Truth: - `assertTrue(boolean condition)` - `assertFalse(boolean condition)` --- template: assertions-1 ### Existence: - `assertNull(Object object)` - `assertNotNull(Object object)` --- template: assertions-1 ### Sameness: - `assertSame(object1, object2)` - `assertNotSame(object1, object2)` --- name: setup-teardown # Setup & Teardown -- ## Overview `xUnit` unit testing frameworks all support the concept of **setup** and **teardown** functions. -- - these are functions that are automatically run by the frameworks before and after the tests are run, respectively -- - they can be used to set up and remove any variables or objects or other dependencies required by the tests -- - most frameworks provide the ability to run such functions before/after an entire test class is instantiated and/or before/after a specific test function is run. -- - this helps reduce code duplication, as multiple tests can share the same setup and teardown logic. --- template: setup-teardown ## Example - Java junit ```java import org.junit.jupiter.api.*; class JUnit5ExampleTest { @BeforeAll static void beforeAll() { System.out.println("Before all tests"); } @BeforeEach void beforeEach() { System.out.println("Before each individual test"); } @AfterEach void afterEach() { System.out.println("After each individual test"); } @AfterAll static void afterAll() { System.out.println("After all tests"); } } // the end ``` --- name: best-practices # Best Practices -- ## TRIPinesss According to "[Pragmatic Unit Testing in Java with JUnit](https://pragprog.com/titles/utj/pragmatic-unit-testing-in-java-with-junit)", by Andy Hunt and Dave Thoma, good unit tests are **TRIP-y**: -- - **T**horough -- - **R**epeatable -- - **I**ndependent -- - **P**rofessional --- template: best-practices ## Three pillars According to Roy Osherove, author of "[The Art of Unit Testing](https://www.manning.com/books/the-art-of-unit-testing)", there are three main pillars of a good unit test: -- - **Trustworthiness** - if you don't believe your test, and find yourself verifying it or ignoring it, it's a sign you should rewrite it. -- - **Maintainability** - a test that is difficult to maintain and takes up more time than it saves might as well be scrapped -- - **Readability** - a test that is not obvious as to what it's testing is a bad test. Tests serve as a form of documentation. -- Listen to [a song about unit testing](https://www.youtube.com/watch?v=fXwWaE1QCS8) by Roy Osherove --- name: doubles # Doubles -- ## Concept Unit tests must run in isolation of one another and of external dependencies, such as databases, APIs, or other servers. -- - Some units of production code normally depends upon an interaction with such a dependency. What to do? -- - Swap out that dependency with the double! - a controlled simulation of the dependency. -- - Using a double will allow you to hone in on problems with your own code, not problems within the external dependency. --- template: doubles ## Types of double There are three commonly-used types of doubles. -- - Mocks -- - Fakes -- - Stubs --- template: doubles ## Mocks Mocks are replacements for external interfaces. -- - The mock assumes the same name and functions as the mocked external dependency. -- - Using a mock can determine only whether the mock has been called or not by production code, how many times it has been called, and whether or not the correct arguments are passed to it when it is called. -- - Production code does not need to be changed to use a mock. It does not "know" that it is not connecting to the real thing. -- - The testing framework takes care of swapping out the external dependency with a mock. --- template: doubles ## Stubs Whereas mocks are called by production code and passed arguments in place of a real external dependency, stubs are used to return pre-defined responses to the calling code. -- - For example, if your app code requires a record to be saved in a database, you might create a mock to intercept the arguments that would normally be sent to the database in place of the real thing, and create a stub that responds with a success or failure message in place of the real response that the database would send, given the supplied parameters. -- - Having a mock send back a stub allows you to use unit tests to determine whether your production code handles the response correctly. --- template: doubles ## Fakes Like, mocks and stubs, fakes are used in the simulation of an external dependency. But whereas mocks and stubs are simple code substitutions for the real thing, a fake is an actually functioning replacement for the external dependency that runs on the local machine. -- - For example, if your code must connect to an API over the Internet, you might create a local HTTP server that simulates that API as your fake so you can test as if it were real. -- - Since unit tests must run independently of one another, any data sent back and forth with the fake will not be saved and will not influence subsequent unit tests that interact with the fake - there is no saved state within the fake. -- - The advantage of using fakes over mocks and stubs is that you get a slightly more "real" test. --- name: tools # Testing Tools -- ## Javascript The following are the recommended tools for unit testing in Node.js Javascript. -- - [mocha](https://www.npmjs.com/package/mocha) as the Javascript testing framework. -- - [chai](https://www.npmjs.com/package/chai) as the assertion library. -- - [chai-htp](https://www.chaijs.com/plugins/chai-http/) - an extension to `chai` that simplifies testing of [express.js](/content/courses/agile-development-and-devops/slides/express) routes. -- - [c8](https://www.npmjs.com/package/c8) or [istanbul](https://istanbul.js.org/) for code coverage analysis. -- - [sinon](https://www.npmjs.com/package/sinon) for stubs, mocks, and fakes. -- - [faker](https://www.npmjs.com/package/faker) for any placeholder data that needs to be generated in tests (e.g. names, email addresses, database data, images, etc) --- template: tools ## Javascript (continued) - To learn more about refactoring your code to be suitable for unit testing, watch the LinkedIn Learning video "[Prepare for Testing](https://www.linkedin.com/learning/node-js-essential-training-web-servers-tests-and-deployment/prepare-for-testing)" -- - To master the syntax of `mocha` and `chai`, watch the "[Writing Unit Tests](https://www.linkedin.com/learning/javascript-test-driven-development-es6/the-purpose-of-unit-testing?u=2131553)" section of videos in the LinkedIn Learning Course, "Javascript: Test-Driven Development". -- - Learn how to make doubles with `sinon` by watching [this video](https://www.youtube.com/watch?v=Qlmv7nox5pM) from the Grace Hopper Full Stack Academy, and reading its [documentation](https://sinonjs.org/#get-started). --- template: tools ## Javascript (continued again) To mock MongoDB databases connected to Javascript projects, try the following: - [mongodb-memory-server](https://github.com/nodkz/mongodb-memory-server), available on [npm](https://www.npmjs.com/package/mongodb-memory-server) --- template: tools ## Python The following are the recommended tools for unit testing in Python. -- - [pytest](https://docs.pytest.org/en/7.2.x/) as the Python testing framework. -- - [pytest-flask](https://pytest-flask.readthedocs.io/en/latest/) - a module that helps test flask routes using pytest -- - [coverage](https://coverage.readthedocs.io/en/latest/) for code coverage analysis. -- - [monkeypatch](https://docs.pytest.org/en/6.2.x/monkeypatch.html) and/or [pytest-mock](https://pytest-mock.readthedocs.io/) for mocking and faking. -- Documentation on each of these sites is reasonably good. --- template: tools ## Python (continued) To mock MongoDB databases connected to Python projects, try the following: - [mongomock](https://pypi.org/project/mongomock/), available [on PyPi](https://pypi.org/project/mongomock/) --- template: tools ## Python (continued again) To learn more about unit testing in Python, watch the LinkedIn Learning video "[Unit Testing and Test Driven Development](https://www.linkedin.com/learning/unit-testing-and-test-driven-development-in-python/welcome?autoplay=true&u=2131553)" --- name: conclusions # Conclusions -- These notes have attempted to give you a high-level overview of the intentions and directions of unit testing. Now go and try it. -- - Thank you. Bye.