A Lifecycle Of Code Under Test
Tags: javascript, code, testing, lifecycle
Published: 2019-07-29 12:30:20

---
title: A Lifecycle Of Code Under Test
published: true
description: I am always concerned that I have "done enough." How do I know that the code is covered appropriately, that odd bugs aren't going to crop up?
tags: JavaScript, Code, Testing, Lifecycle
series: Front-End Testing
cover_image: https://thepracticaldev.s3.amazonaws.com/i/j7syeh0zsp9tx0hjj4ld.jpg
---
This article was written in conjunction with [Dan Wypiszynski](https://dev.to/dwyp) (one of my co-workers).

Here's the original version of this article / talk ...

![Whiteboard documentation of this Article / Talk](https://thepracticaldev.s3.amazonaws.com/i/u53ad2sbzw4momh0va66.jpg)

When examining of front-end code patterns these days, developers should also equate testing as a part of the development cycle.

This article is about the unit and integration tests that a developer writes to test their code immediately, not about higher level tests that are written by a QA individual or department. In this article, I will set aside the "chicken or the egg" discussion of Test Driven Development and/or Behavior Driven Development. For a higher level view, see ...

* [Unit and Integration Testing](https://dev.to/rfornal/unit-and-integration-testing-bco)
* [Shades of Gray in Front-End Testing](https://dev.to/rfornal/shades-of-gray-in-front-end-testing-3li1)

I want to focus on the code and tests more holistically at each lifecycle stage.

When thinking about these tests, I am always concerned that I have "done enough." How do I know that the code is covered appropriately, that odd bugs aren't going to crop up? Did I cover all the use cases and what if someone changes my code down-the-road?

There is a definite and clear lifecycle to testing ...

1. Define Inputs and Outputs
2. Write Initial Test Coverage
3. Handling Bug Coverage
4. Refactoring
5. Abstraction
6. Future Work (how are tests affected?)

The code used here is closer to pseudocode than actual JavaScript (this means that I did not run the code or tests to see if the work). The code is here to illustrate the points being discussed.

## Base Code

To examine the code-test lifecycle, assume the following is some overly complicated chunk of code we've written or want to write (BDD or TDD).

This function ***overlyComplicated*** should take two strings (**a** and **b**), adds them together, then returns the first **len** characters of the resulting string.

The "complicated" and "injected" bug parts are intentional; allowing the lifecycle to have a path forward.

```javascript
function overlyComplicated(a, b, len) {
  var sum = "";

  if (len < 1) {
    return "";
  }

  for (var i = 0; i < a.length; i++) {
    sum = sum + a[i];
  }
  for (var i = 0; i < b.length; i++) {
    sum = sum + b[i];
  }

  // "INJECTED" BUG HERE
  if (len === 2 || len === 4 || len === 6) {
    return "unexpected";
  }

  return sum.subsrt(0, len);
}

var oC = overlyComplicated;
```

## Define Inputs and Outputs

Looking at the code: ***function overlyComplicated(a, b, len)*** and ***return sum.substr(0, len)***, we can begin to define the inputs and outputs of this function.

### Inputs

* **a**: string of some length.
* **b**: string of some length.
* **len**: number (integer) of characters of the combined to return.

### Outputs

* string of "len" characters.

### Examples

* ("abc", "def", 0) returns ""
* ("abc", "def", 1) returns "a"
* ("abc", "def", 3) returns "abc"
* ("abc", "def", 5) returns "abcde"

## Write Initial Testing Coverage

### All Branches (paths)

* There are no branches in this code; each should be covered if they exist.

### Positive Testing

Positive testing, often times referred to as **Happy Path Testing** is generally the first form of testing that a developer will perform on some code. It is the process of running typical scenarios against the code. Hence as implied, positive testing entails running a test scenario with only correct and valid data.

* expect(oC("abc", "def", 1)).toEqual("a");
* expect(oC("abc", "def", 3)).toEqual("abc");
* expect(oC("abc", "def", 5)).toEqual("abcde");

### Negative Testing

Negative testing commonly referred to as **Error Path Testing** or **Failure Testing** is generally done to ensure the stability of the code.

This is the process of applying creativity and validating the code against invalid data. This means its intended purpose is to check if the errors are being handled gracefully.

For this code, we will only examine the result of a non-positive value for the **len** input.

* expect(oC("abc", "def", 0)).toEqual("");
* expect(oC("abc", "def", -1)).toEqual("");

## Handling Bug Coverage

Here, examining the "bug" code ...

```javascript
// "INJECTED" BUG HERE
if (len === 2 || len === 4 || len === 6) {
  return "unexpected";
}
```

### Repeat The Bug In Test Form ...

expect(oC("abc", "def", 2)).toEqual("ab");

* Returns ... expect "unexpected" to equal "ab".

expect(oC("abc", "def", 4)).toEqual("abcd");

* Returns ... expect "unexpected" to equal "abcd".

expect(oC("abc", "def", 6)).toEqual("abcdef");

* Returns ... expect "unexpected" to equal "abcdef".

### Fixing The Bug

After removing the "bug" code ...

```javascript
function overlyComplicated(a, b, len) {
  var sum = "";

  if (len < 1) {
    return "";
  }

  for (var i = 0; i < a.length; i++) {
    sum = sum + a[i];
  }
  for (var i = 0; i < b.length; i++) {
    sum = sum + b[i];
  }

  // "INJECTED" BUG HERE
  // if (len === 2 || len === 4 || len === 6) {
  //   return "unexpected";
  // }

  return sum.substr(0, len);
}

var oC = overlyComplicated;
```

All three tests should now be passing ...

* expect(oC("abc", "def", 2)).toEqual("ab");
* expect(oC("abc", "def", 4)).toEqual("abcd");
* expect(oC("abc", "def", 6)).toEqual("abcdef");

## Refactoring

To show a simple refactor, let's comment out the "overly complicated" part of the code and build a much simpler form.

```javascript
function overlyComplicated(a, b, len) {
  var sum = "";

  if (len < 1) {
    return "";
  }

  sum = a + b;
  sum = sum.substr(0, len);
  return sum;

  // for (var i = 0; i < a.length; i++) {
  //   sum = sum + a[i];
  // }
  // for (var i = 0; i < b.length; i++) {
  //   sum = sum + b[i];
  // }

  // return sum.substr(0, len);
}

var oC = overlyComplicated;
```

Based on this code change, all previous tests should be expected to still pass. If there was no coverage, the developer needs to take every step to cover the code, as-is, unchanged.

### Positive Tests

* expect(oC("abc", "def", 1)).toEqual("a");
* expect(oC("abc", "def", 3)).toEqual("abc");
* expect(oC("abc", "def", 5)).toEqual("abcde");

### Negative Tests

* expect(oC("abc", "def", 0)).toEqual("");
* expect(oC("abc", "def", -1)).toEqual("");

### Bug Tests

* expect(oC("abc", "def", 2)).toEqual("ab");
* expect(oC("abc", "def", 4)).toEqual("abcd");
* expect(oC("abc", "def", 6)).toEqual("abcdef");

### Abstraction

Now, let's examine an abstraction within this code ...

```javascript
function getSum(a, b) {
  return a + b;
}

function overlyComplicated(sumFn, a, b, len) {
  var sum = "";

  if (len < 1) {
    return "";
  }

  sum = sumFn(a, b).substr(0, len);
  // sum = a + b;
  // sum = sum.substr(0, len);
  return sum;
}

function oC(a, b, len) {
  return overlyComplicated(getSum, a, b, len);
}
```

Given this code change, all previous tests should still pass. But, we can now add testing against the ***getSum*** function.

* expect(getSum("abc", "dev")).toEqual("abcdef");

Certainly, more testing can be done: more positive testing, as well as negative testing. Additionally, ***overlyComplicated*** is now given a means to mock, replace, or spy on the ***getSum*** function, if needed.

## Future Work (how are tests affected?)

Starting with the abstraction just applied, what if someone comes along in the future and adds a global object and injects a line of code into the ***overlyComplicated*** function to add the sum to the object.

```javascript
var global = {};

function getSum(a, b) {
  return a + b;
}

function overlyComplicated(sumFn, a, b, len) {
  var sum = "";

  if (len < 1) {
    return "";
  }

  sum = sumFn(a, b).substr(0, len);
  global.sum = sum;
  return sum;
}

function oC(a, b, len) {
  return overlyComplicated(getSum, a, b, len);
}

```

### How Are Tests Affected?

* Per **Black-Box Testing**, no test should fail (purely examining inputs to outputs).
* Per **White-Box Testing**, tests should be written to cover the new code.

## Future Work Tests

... given

* oC("abc", "def", 1);
... then

* expect(global.sum).toEqual("a");

## Summary

The definite and clear lifecycle in testing encompasses ...

1. Define Inputs and Outputs
2. Write Initial Test Coverage
3. Handling Bug Coverage
4. Refactoring
5. Abstraction
6. Future Work (how are tests affected?)

A proper level of testing will ensure a developer ...

1. Has "done enough."
2. Knows the code is covered appropriately.
3. Is secure in the knowledge that odd bugs don't continue to exist
4. And that the code will survive future changes.

Testing, applied in a methodical manner, will allow a developer ...

* To have CONFIDENCE in code being released without defects,
* And PROOF that it works.