Try   HackMD

Clean Code - 9 Unit Tests

Writing automated unit tests is encouraged because of the trend of Agile and TDD movements. However, it is important to write good tests instead of just writing tests.

The Three Laws of TDD

Other resources:
https://blog.cleancoder.com/uncle-bob/2014/12/17/TheCyclesOfTDD.html
https://www.ibm.com/garage/method/practices/code/practice_test_driven_development/

The tests and the production code are written together, with the tests just a few seconds ahead of the production code.

First Law

You may not write production code until you have written a failing unit test.

Second Law

You may not write more of a unit test than is sufficient to fail, and not compiling is failing.

Third Law

You may not write more production code than is sufficient to pass the currently failing test.

Example

Phase 1
Write a first failing unit test:

// production codes
// first method
function getNumbersFromStringKey(sourceStr: string) : number {
    
}

// below are unit tests
function testGetNumbersFromStringKeyWithNumberStr() : void {
    const result = getNumbersFromStringKey('010789666');
    expect(!Number.isNaN(result)).toEqual(true);    
}

Phase 2
Since the unit test testGetNumbersFromStringKeyWithSpaces will fail, should modify the production codes to pass the test case, instead of creating more test cases:

// production codes
// first method
function getNumbersFromStringKey(sourceStr: string) : number {   
    return +sourceStr;
}

// below are unit tests
function testGetNumbersFromStringKeyWithNumberStr() : void {
    const result = getNumbersFromStringKey('010789666');
    expect( !Number.isNaN(result) ).toEqual(true);    
}

function testGetNumbersFromStringKeyWithSpaces() : void {
    const result = getNumbersFromStringKey('010 789  666');
    expect( !Number.isNaN(result) ).toEqual(true);    
}

Phase 3
Can start to write another production method after all unit tests pass:

// production codes
// first method
function getNumbersFromStringKey(sourceStr: string) : number {   
    const result = +sourceStr.replace(/\D/g, "");
    return result;
}

// below are unit tests
function testGetNumbersFromStringKeyWithNumberStr() : void {
    const result = getNumbersFromStringKey('010789666');
    expect( !Number.isNaN(result) ).toEqual(true);    
}

function testGetNumbersFromStringKeyWithSpaces() : void {
    const result = getNumbersFromStringKey('010 789  666');
    expect( !Number.isNaN(result) ).toEqual(true);    
}


function testGetNumbersFromStringKeyWithNotNumber() : void {
    const result = getNumbersFromStringKey('010*789++666');
    expect( !Number.isNaN(result) ).toEqual(true);    
}

Keeping Tests Clean

When tests are dirty, it's almost the same as no tests. Discipline is:

Test code is just as important as production code.

How dirty tests affect production codes

  • Scenario 1: When updating the function body of the production codes (still the same API, only change the body of a function)
    • The dirty tests will not fail since the goal of functions does not change
  • Scenario 2: When updating the entire API (for example, new functions take more parameters, or some functions no longer exist)
    • The dirty tests will fail and it takes lots of effort to fix them to pass the new functions

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Tests Enable the -ilities

Keeping unit tests clean helps keep production code flexible and makes changing it easier and more confident.

Clean Tests

The clean test is all about readability. Readability includes:

  • Clarity
  • Simplicity
  • Density of expression: eg, if, while, should not have too many, should not be too complicated

Bad unit tests example in Jave

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Same unit test after refactor

Use the BUILD-OPERATE-CHECK pattern
http://fitnesse.org/FitNesse.FullReferenceGuide.UserGuide.WritingAcceptanceTests.AcceptanceTestPatterns.BuildOperateCheck

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Domain-Specific Testing Language

Write your own API for test use, and you need to constantly update and refactor according to test needs.

Example

Before having APIs:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

After writing and using the APIs:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

A Dual Standard

While APIs for testing needs to be as simple and clean as production code, they don't need to be as efficient as production code. The reason is that the tests are run in a test environment, which has different requirements than the production environment.

Example

Need to move eyes back and forth to read the test:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Increase the readability:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

How the getState method is implemented:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

One Assert per Test

Sometimes the same code can appear in multiple tests when there is only one assert in each test:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Template Method

To solve the above duplication of code problem, can use the Template Method.
Source https://www.artima.com/weblogs/viewpost.jsp?thread=35578

Use Given, When, Then:

  • Given
    • preconditions, things should be done before tests, and be written as a function
  • When
    • actions
  • Then
    • verification

Example in pseudocodes:

Scenario: test sign-in user checkout
    Given: create an account and sign in
    When: put a product in the cart and checkout
    Then: the order summary has the user and product information

Single Concept per Test

Test a single concept in each test function. When there are multiple concepts in a test:

The scenarios and preconditions in the above test:

Given the last day of a month with 31 days (like May):

Scenario 1:
Add 1 month, the last day of the new month should be 30th
Scenario 2:
Add 2 months, the last day of the new month should be 31st

Given the last day of a month with 30 days in it (like June):

Scenario 3:
Add 1 month, the last day of the new month should be 30th

The above test should be divided into 3 tests. Should also include other missing tests like the last day of a month with 28 days in it (like February)

F.I.R.S.T

Fast

Tests should be fast. They should run quickly.

Independent

Tests should not depend on each other.

Repeatable

Tests should be repeatable in any environment.

For example, tests should be able to run on both the production environment and the OA environment.

Self-Validating

The tests should have a boolean output. Either they pass or fail.

Timely

The tests need to be written in a timely fashion. Unit tests should be written just before the production code that makes them pass.

Conclusion

We should always keep our tests clean.

tags: learn clean code test automation