Test-Driven
Development
© 2017 Morgan C. Benton code4your.life
Test-Driven
Development
Test-driven Development (TDD) is a style of writing software that relies on short iterations that start with the writing of automated tests. It has become dominant among most software development teams, and is credited for saving enormous amounts of time and money in the software development lifecycle.
© 2017 Morgan C. Benton code4your.life
Why TDD?
- Proves your code works
- Provides low-level regression test suite
- Allows for improving design without breaking
- It's more fun!
- Demonstrates progress at a granular level
- Serves as example code
- Forces you to plan first
- Reduces cost of bugs
- Better than code inspection
- Eliminates coder's block
- Makes for better design
- It's faster!
© 2017 Morgan C. Benton code4your.life
Key Question:
How will I know when I have solved the problem???
© 2017 Morgan C. Benton code4your.life
Confusing Terminology?
There's a lot of confusion/disagreement about different types of tests and different types of TDD
- TDD vs BDD vs ATDD
- Types of tests:
- Unit: low-level
- Integration
- Acceptance
Take home message: make sure you know what your team means by all of these terms
© 2017 Morgan C. Benton code4your.life
What to Test?
- You should only be testing code that you write
- Do NOT test code that comes from external or 3rd party libraries (they should be writing their own tests)
- Tests should NOT rely upon external resources, e.g. a network connection, database, filesystem, etc.
© 2017 Morgan C. Benton code4your.life
Popular Testing Harnesses/Frameworks
© 2017 Morgan C. Benton code4your.life
Language | Framework |
---|---|
JavaScript | Jasmine, Mocha, Jest |
Python | py.test, nose, doctest |
R | RUnit, testthat |
Ruby | Minitest, RSpec |
PHP | PHPUnit, Codeception, Behat, PHPSpec |
Java | JUnit, JTest, Arquillian |
Go | testing, testify, Ginkgo, Gomega |
Example:
sum(a, b)
- Assume we have a function, sum(a, b), that accepts two parameters and returns their arithmetic sum
- Each example shows:
- The sum() code in its own file
- The tests to check the correctness of sum() in a separate file
- A screenshot of the output of running the test from the command line
© 2017 Morgan C. Benton code4your.life
Javascript/Jest
// sum.js
/**
* Computes the sum of two numbers.
* @param {number} a The first number
* @param {number} b The second number
* @return {number} The computed sum
*/
const sum = (a, b) => {
return a + b;
}
module.exports = sum;
© 2017 Morgan C. Benton code4your.life
// sum.spec.js
// import the sum.js library
const sum = require('./sum');
describe('An arithmetic library', () => {
it('can compute sums', () => {
expect(sum(3, 4)).toBe(7);
})
});
Python/Pytest
def sum(a, b):
"""
Compute the sum of two
numbers, a and b
"""
return a + b
© 2017 Morgan C. Benton code4your.life
# import code to be tested
from sum import sum
# test the sum() function
def test_sum():
assert sum(3, 4) == 7
R/Testthat
# sum.R
my_sum <- function(a, b) {
return(a + b)
}
© 2017 Morgan C. Benton code4your.life
# tests/test_sum.R
# import library to be tested
source("../sum.R")
context("Arithmetic library")
test_that("my_sum() adds 2 numbers", {
expect_that(my_sum(3, 4), equals(7))
})
# run_tests.R
# import testing library
library(testthat)
# run files named ./tests/test*.R
test_dir("./tests", reporter = "progress")
Ruby/MiniTest
© 2017 Morgan C. Benton code4your.life
# sum.rb
# Computes the sum of two numbers
def sum(a, b)
return a + b
end
# sum.spec.rb
# import testing library
require 'minitest/autorun'
# import code to be tested
require_relative 'sum'
describe "An arithmetic library" do
it "can compute sums" do
sum(3, 4).must_equal 7
end
end
PHP/PHPUnit
© 2017 Morgan C. Benton code4your.life
<?php // sum.php
/**
* Calculates the sum of two numbers
* @param number $a The first number
* @param number $b The second number
* @return number The
*/
function sum($a, $b) {
return $a + $b;
}
<?php // test_sum.php
use PHPUnit\Framework\TestCase;
// import code to be tested
require_once 'sum.php';
final class ArithmeticTest extends TestCase {
public function testSum() {
$this->assertEquals(sum(3, 4), 7);
}
}
Java/JUnit
© 2017 Morgan C. Benton code4your.life
// Arithmetic.java
public class Arithmetic {
public static double sum(double a, double b) {
return a + b;
}
}
// ArithmeticTests.java
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class ArithmeticTests {
@Test
public void sumTest() {
assertEquals(Arithmetic.sum(3, 4), 7, 0.0);
}
}
Go
© 2017 Morgan C. Benton code4your.life
package main
func sum(a float32, b float32) float32 {
return a + b
}
func main() {}
package main
import "testing"
func TestSum(t *testing.T) {
total := sum(3, 4)
if total != 7 {
t.Error("Sum fail")
}
}
Example:
getTitle(url)
- Assume we have a function, getTitle(url), that accepts a URL and returns the content of the <title> tag of the HTML page to which the URL points
- Each example shows:
- The getTitle(url) code in its own file
- The tests to check the correctness of getTitle()
- Sometimes a third file is necessary to make it work
- A screenshot of the output of running the test from the command line
- NOTE: The tests do NOT attempt to test the HTTP request since that would be testing someone else's code. Instead they all use a mock or stub to fake the HTTP call
© 2017 Morgan C. Benton code4your.life
Javascript/Jest
// get-title.js
// 3rd party library for HTTP requests
const rp = require('request-promise');
const getTitle = url => {
return rp.request(url)
.then(html => {
const regex = /<title>(.*?)<\/title>/;
let title = html.match(regex);
return title[1];
});
}
module.exports = getTitle;
© 2017 Morgan C. Benton code4your.life
// get-title.spec.js
const getTitle = require('./get-title');
describe('An HTML parsing library', () => {
it('can get the <title>', () => {
expect(getTitle('http://example.com'))
.resolves.toBe('Cool Title, Bro');
});
});
// __mocks__/request-promise.js
const rp = jest.genMockFromModule('request-promise');
const request = url => {
return new Promise((resolve, reject) => {
resolve(`
<html>
<head>
<title>Cool Title, Bro</title>
</head>
<body>
<p>yo</p>
</body>
</html>
`);
});
};
rp.request = request;
module.exports = rp;
Python/Pytest
# get_title.py
import requests # HTTP request library
import re # regular expressions
def get_title(url):
"""Get the <title> from the HTML at the url"""
r = requests.get(url)
p = re.compile('<title>(.*?)</title>')
s = p.search(r.text)
return s.group(1)
© 2017 Morgan C. Benton code4your.life
# test_get_title.py
# import code to be tested
from get_title import get_title
import requests
import pytest
@pytest.fixture
def patched_requests(monkeypatch):
def mocked_get(uri, *args, **kwargs):
r = type('MockedReq', (), {})()
r.text = '''
<html>
<head>
<title>Cool Title, Bro</title>
</head>
<body>
<p>yo</p>
</body>
</html>
'''
return r
monkeypatch.setattr(requests, 'get', mocked_get)
# test the get_title() function
def test_get_title(patched_requests):
title = get_title("http://example.com")
assert title == "Cool Title, Bro"
R/Testthat
# get_title.R
library(httr)
library(stringr)
get_title <- function(url) {
res <- GET(url)
html <- content(res, "text")
match <- str_match(
pattern = '<title>(.*?)</title>',
string = html
)
match[1,2]
}
© 2017 Morgan C. Benton code4your.life
# tests/test_get_title.R
# import code to be tested
source("../get_title.R")
# load library for faking HTTP requests
library(httptest)
context("HTML Parsing Library")
test_that("get_title() can parse <title>", {
with_mock(
GET = function(url) {
fake_response(url, content = '<html>
<head>
<title>Cool Title, Bro</title>
</head>
<body>
<p>Yo</p>
</body>
</html>')
},
expect_that(
get_title('http://example.com'),
equals("Cool Title, Bro")
)
)
})
# run_tests.R
# import testing library
library(testthat)
# run files named ./tests/test*.R
test_dir("./tests", reporter = "progress")
Ruby/MiniTest
© 2017 Morgan C. Benton code4your.life
# get_title.rb
require "http"
# Gets the <title> from HTML
def get_title(url)
html = HTTP.get(url).body
p = /<title>(.*?)<\/title>/
m = p.match(html)
return m[1]
end
# get_title.spec.rb
# import testing library
require 'minitest/autorun'
require 'webmock/minitest'
# import code to be tested
require_relative 'get_title'
body = <<HTML
<html>
<head>
<title>Cool Title, Bro</title>
</head>
<body>
<p>yo</p>
</body>
</html>
HTML
describe "An HTML parsing library" do
it "can get the <title> content" do
stub_request(:any, "http://example.com").
to_return(body: body)
get_title("http://example.com")
.must_equal "Cool Title, Bro"
end
end
PHP/PHPUnit
© 2017 Morgan C. Benton code4your.life
<?php // get_title.php
namespace Morphatic;
/**
* Gets the text from the <title> tag
* @param string $url The page URL
* @return string The <title> text
*/
function get_title($url) {
$html = file_get_contents($url);
preg_match('/<title>(.*?)<\/title>/', $html, $matches);
return $matches[1];
}
<?php // test_get_title.php
namespace Morphatic;
use PHPUnit\Framework\TestCase;
// import code to be tested
require_once 'get_title.php';
// mock file_get_contents()
function file_get_contents($url) {
return <<<HTML
<html>
<head>
<title>Cool Title, Bro</title>
</head>
<body>
<p>yo</p>
</body>
</html>
HTML;
}
final class HTMLParserTest extends TestCase {
public function testGetTitle() {
$this->assertEquals(
get_title('http://example.com'),
'Cool Title, Bro'
);
}
}
Java/JUnit
© 2017 Morgan C. Benton code4your.life
// HTMLParser.java
import java.io.IOException;
import com.google.api.client.http.apache.ApacheHttpTransport;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpResponse;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class HTMLParser {
private HttpTransport transport;
public HTMLParser() {
transport = new ApacheHttpTransport();
}
public HTMLParser(HttpTransport xport) {
transport = xport;
}
public String getTitle(String url) throws IOException {
GenericUrl gurl = new GenericUrl(url);
return getTitle(gurl);
}
public String getTitle(GenericUrl url) throws IOException {
HttpRequest req = transport.createRequestFactory().buildGetRequest(url);
HttpResponse res = req.execute();
String html = res.parseAsString();
Pattern p = Pattern.compile("<title>(.*?)</title>");
Matcher m = p.matcher(html);
m.find();
return m.group(1);
}
}
// HTMLParserTests.java
import java.io.IOException;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
import com.google.api.client.testing.http.HttpTesting;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.testing.http.MockHttpTransport;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.LowLevelHttpRequest;
import com.google.api.client.testing.http.MockLowLevelHttpRequest;
import com.google.api.client.http.LowLevelHttpResponse;
import com.google.api.client.testing.http.MockLowLevelHttpResponse;
public class HTMLParserTests {
@Test
public void getTitleTest() {
HttpTransport transport = new MockHttpTransport() {
@Override
public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
return new MockLowLevelHttpRequest() {
@Override
public LowLevelHttpResponse execute() throws IOException {
MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
response.setContent(
"<html>\n" +
" <head>\n" +
" <title>Cool Title, Bro</title>\n" +
" </head>\n" +
" <body>\n" +
" <p>yo</p>\n" +
" </body>\n" +
"</html>"
);
return response;
}
};
}
};
HTMLParser p = new HTMLParser(transport);
String title = "";
try {
title = p.getTitle(HttpTesting.SIMPLE_GENERIC_URL);
} catch (IOException e) {
System.out.println(e);
}
assertEquals(
"Cool Title, Bro",
title
);
}
}
Go
© 2017 Morgan C. Benton code4your.life
// htmlparser.go
package htmlparser
import (
"net/http"
"io/ioutil"
"regexp"
)
func getTitle(url string) string {
resp, err := http.Get(url)
if err != nil { /* handle error */ }
body, err := ioutil.ReadAll(resp.Body)
if err != nil { /* handle error */ }
re := regexp.MustCompile(`<title>(.*?)</title>`)
matches := re.FindStringSubmatch(string(body));
return matches[1]
}
func main() {}
// htmlparser_test.go
package htmlparser
import (
"testing"
"gopkg.in/jarcoal/httpmock.v1"
)
func TestGetTitle(t *testing.T) {
body :=
`<html>
<head>
<title>Cool Title, Bro</title>
</head>
<body>
<p>yo</p>
</body>
</html>`
httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpmock.RegisterResponder("GET", "http://example.com",
httpmock.NewStringResponder(200, body))
title := getTitle("http://example.com")
if title != "Cool Title, Bro" {
t.Error(title + " != Cool Title, Bro")
}
}
CoveraGe
The percentage of lines of source code in your project that are covered by tests. Target 90%. Not only do you want people to know your code is tested, but that ALL (or most) of it has been tested.
© 2017 Morgan C. Benton code4your.life
Summary
Testing is an ABSOLUTELY CRITICAL skill to have as a developer these days. You will 100% be asked to write tests if you get hired as a programmer. It's a hard habit to build if you don't start out doing it from the very beginning. Be smart. Get a head start and adopt the practice. You'll be glad you took on the extra frustration.
© 2017 Morgan C. Benton code4your.life
Test-Driven Development
By Morgan Benton
Test-Driven Development
Introduction to the test-driven development (TDD) in computer programming. Example code in a variety of languages is provided to demonstrate TDD.
- 1,493