## Unit Testing Made Simple Benjamin Newman - btdn@acm.org Bitreich Con 2019 ## About Your Presenter * Full-time quality assurance "engineer" * Part-time master's student in computer science * Someone who can’t say, "No", to Christoph... ## Naïve SDLC for a Software Developer 1. Determine what the business people _need_ (not want) the computer to do. 2. Making the computer do that. ## A Day in My Work-Life: Quality Assurance Making sure that: 1. The _business people_ know what they _need_ the computer to do. 2. The _programmer people_ know what the computer needs to do. 3. The _computer_ actually does that. ## The Computer Actually Does That? Two kinds of (dynamic) verification: 1. Manual testing * Boring and expensive * Hopefully done only once 2. Automated testing * Quick and cheap * Can be done frequently ## The Testing Pyramid * A model for automated tests * At the bottom: fastest and most isolated * At the top: slowest and least isolated ^. / \*. /UI \**. /_____\***. / Inte- \****. / gration \****| /___________\***| / \**| / Unit \*| /_________________\| KCK, http://ascii.co.uk/art/pyramid ## More Advantages of Unit Testing _All_ automated tests: * Confidence when changing implementation * Easily repeatable Unit tests: * More helpful when they fail * Find bugs earlier * Can improve the implementation * More modular * Documentation MORE RELIABLE IMPLEMENTATION! ## Contrived Example: The Problem Need a function, is_odd: * Input: a number * Output: True if and only if the input is odd ## Contrived Example: Stub Implementation and Test Cases TDD-esque (Test-Driven Development) assert(n, expected) → if (is_odd(n) = expected) PASS else FAIL assert(-1, TRUE) assert(0, FALSE) assert(3, TRUE) assert(4, FALSE) is_odd(n) → FALSE ## Contrived Example: Test the Stub is_odd(n) → FALSE assert(-1, TRUE) → FAIL assert(0, FALSE) → PASS assert(3, TRUE) → FAIL assert(4, FALSE) → PASS ## Contrived Example: The (Initial, Incorrect) Implementation is_odd(n) → (n%2 = 0) ## Contrived Example: Initial Results is_odd(n) → (n%2 = 0) assert(-1, TRUE) → FAIL assert(0, FALSE) → FAIL assert(3, TRUE) → FAIL assert(4, FALSE) → FAIL Oh, no! ## Contrived Example: Fix the Implementation is_odd(n) → (n%2 = 1) ## Contrived Example: Re-Run the Tests is_odd(n) → (n%2 = 1) assert(-1, TRUE) → PASS assert(0, FALSE) → PASS assert(3, TRUE) → PASS assert(4, FALSE) → PASS Much better! ## How to Start? Find a Framework: 1. In the codebase 2. In the ecosystem 3. Roll your own This is the big hurdle. ## When to Write Tests 1/2 All the time: * Writing new code/adding features * Refactoring ## When to Write Tests 2/2 | | Fixing bugs \\_V_// \/=|=\/ [=v=] BEFORE __\___/_____ /..[ _____ ] and /_ [ [ M /] ] /../.[ [ M /@] ] After <-->[_[ [M /@/] ] /../ [.[ [ /@/ ] ] _________________]\ /__/ [_[ [/@/ C] ] <_________________>>0---] [=\ \@/ C / / ___ ___ ]/000o /__\ \ C / / \ / /....\ \_/ / ....\||/.... [___/=\___/ . . . . [...] [...] . .. . [___/ \___] . 0 .. 0 . <---> <---> /\/\. . . ./\/\ [..] [..] / / / .../| |\... \ \ \ _[__] [__]_ / / / \/ \ \ \ [____> <____] https://www.asciiart.eu/computers/bug ## What to Test (Non-Exhaustive Examples) * Happy paths * Sad paths (input that cause errors) * Boundary conditions * BUG FIXES . ' . ' .( '.) ' _ ('-.)' (`'.) ' |0|- -(. ')`( .-`) (-') .--`+'--. . (' -,).(') . |`-----'| (' .) - ('. ) | | . (' `. ) | .-. | ` . ` | (0.0) | | >|=|< | | `"` | | | jgs| | `-.___.-' https://asciiart.website/index.php?art=animals/insects/other ## Good Tests in a Nutshell Are useful: * Simple * Failable * Independent ## Unit Test Smells Are not useful: * Complicated * Multiple concerns * Unreliable * Define implementation ## Tell Me More About this Art Bowes, David, Tracy Hall, Jean Petrić, Thomas Shippey, and Burak Turhan. 2017. “How Good Are My Tests?” In _Proceedings of the 8th Workshop on Emerging Trends in Software Metrics_, 9–14. WETSoM ’17. Piscataway, NJ, USA: IEEE Press. https://doi.org/10.1109/WETSoM.2017.2. (https://bura.brunel.ac.uk/bitstream/2438/14816/1/FullText.pdf) * 16 principles of unit tests * Metrics * Test smells ## Questions? ___ ___ ___ ___ |__ \ |__ \ |__ \ |__ \ / / / / / / / / |_| |_| |_| |_| (_) (_) (_) (_) ## The Main Question Was.... Question: How do you start writing tests if you already have a big codebase and it's not super-simple to use? For example, if it needs an N amount of daemons running, and requires some kind of continuous user input. ------ Answer: If it would require n>0 daemon to be running, it would be an integration test. User input can be mocked, but "continuous user input" makes it sound like an integration test as well. Even if unit testing were feasible, integration (and UI) testing still have value; in this case, however, integration testing is even more valuable than it would be otherwise. In addition to the advantages of automated testing if the codebase continues as it is, it would also reduce the risk if it was decided to to refactor or rewrite the implementation such that unit testing would be possible. In any case, if the implementation has lots of smells, the test suite will as well.