Discovering the new Digital world

15 Sep 2017

Software Quality - Hands-on TDD in R using the 'testthat' package

“Test-driven development (TDD) is a software development process that relies on the repetition of a very short development cycle: requirements are turned into very specific test cases, then the software is improved to pass the new tests, only. This is opposed to software development that allows software to be added that is not proven to meet requirements.”, from TDD - Wikipedia

This is an hands-on exercise on using TDD with R to solve a simple problem: implement a roman number conversion algorithm. The main focus is on the practice of Test Driven Development and not the code/ algorithm created.

The outcome of this practice can be found in this repository. The following artifacts have been created

In order to be able to run the example the following packages need to be locally installed

#Using the CRAN repo
install.packages("checkpoint")
install.packages("testthat")

More information on how the testthat package can be found in the following blog “TDD and unit testing in R using the ‘testthat’ package”

Assumption

TDD approach: step by step

Starting Up

Think about the design of the code, in this case the function that we want to develop

Implement the shell of the function , with a very simplicistic implementation

convert_to_roman <- function(num){
  return("I")
}

The TDD mindset, before writing any code

Applied Incremental Design, one small step at a time

Step1, the argument must be integer otherwise an exception is thrown

(1) Create a test passing as argument a non numeric/ integer (e.g. 1) - expecting an error

#Add expectation
context("Input Argument Validity")

test_that("passing a non numeric/ integer an exception is thrown",{
  expect_error(object = convert_to_roman(1))
})
}
#Modify the code
convert_to_roman <- function(num){
  if(!is.integer(num)) stop("must be integer")
  return("I")
}
#Add expectations
context("Input Argument Validity")

test_that("passing a non numeric/ integer an exception is thrown",{
  expect_error(object = convert_to_roman(1))
  expect_error(object = convert_to_roman(3.14))
})
#Add expectations
context("Input Argument Validity")

test_that("passing a non numeric/ integer an exception is thrown",{
  expect_error(object = convert_to_roman(1))
  expect_error(object = convert_to_roman(3.14))
  expect_error(object = convert_to_roman("test"))
})

(2) Create a test passing as argument a numeric/ integer (e.g. 1L) - expecting roman numeral “I”

context("Input Argument Validity")

test_that("passing a non numeric/ integer an exception is thrown",{
  expect_error(object = convert_to_roman(1))
  expect_error(object = convert_to_roman(3.14))
  expect_error(object = convert_to_roman("test"))
})

test_that("passing a numeric/ integer no exception thrown",{
  expect_match(object = convert_to_roman(1L), regexp = "^I$")
})

Step2, passing an integer the correct roman numeral is returned

(3) Create a test passing as argument an integer between [1..3]

#Create a new context, a new test and add expectation
context("Roman Numeral Conversion")

test_that("passing an integer between [1..3] the correct roman numeral is returned",{
  expect_match(object = convert_to_roman(1L), regexp = "^I$")
})
#Create a new context, a new test and add expectation
context("Roman Numeral Conversion")

test_that("passing an integer between [1..3] the correct roman numeral is returned",{
  expect_match(object = convert_to_roman(1L), regexp = "^I$")
  expect_match(object = convert_to_roman(2L), regexp = "^II$")
})
#Modify the code
convert_to_roman <- function(num){
  if(!is.integer(num)) stop("must be integer")
  if (num == 1L) return("I")
  if (num == 2L) return("II")
}

Repeat for 3 return “III”: add expectation (make the test fail) and modify the code (make the test pass)

#Create a new context, a new test and add expectation
context("Roman Numeral Conversion")

test_that("passing an integer between [1..3] the correct roman numeral is returned",{
  expect_match(object = convert_to_roman(1L), regexp = "^I$")
  expect_match(object = convert_to_roman(2L), regexp = "^II$")
  expect_match(object = convert_to_roman(3L), regexp = "^III$")
})
#Modify the code
convert_to_roman <- function(num){
  if(!is.integer(num)) stop("must be integer")
  if (num == 1L) return("I")
  if (num == 2L) return("II")
  if (num == 3L) return("III")
}

(4) Create a test passing as argument a particular integer 4

#Add a new test and add expectation
context("Roman Numeral Conversion")

test_that("passing an integer between [1..3] the correct roman numeral is returned",{
  expect_match(object = convert_to_roman(1L), regexp = "^I$")
  expect_match(object = convert_to_roman(2L), regexp = "^II$")
  expect_match(object = convert_to_roman(3L), regexp = "^III$")
})

test_that("passing an integer 4 the correct roman numeral is returned",{
  expect_match(object = convert_to_roman(4L), regexp = "^IV$")
})
#Modify the code
convert_to_roman <- function(num){
  if(!is.integer(num)) stop("must be integer")
  if (num == 1L) return("I")
  if (num == 2L) return("II")
  if (num == 3L) return("III")
  if (num == 4L) return("IV")
}

(5) Create a test passing as argument a particular integer 5

Continue one simple step at a time …..

Note that the code implementing that function is shaped incrementally, one smal step after the other. Using this simple approach the final tests and code will look like

#Test for the conversion [1..10]
context("Roman Numeral Conversion")

test_that("passing an integer between [1..3] the correct roman numeral is returned",{
  expect_match(object = convert_to_roman(1L), regexp = "^I$")
  expect_match(object = convert_to_roman(2L), regexp = "^II$")
  expect_match(object = convert_to_roman(3L), regexp = "^III$")
})

test_that("passing an integer 4 the correct roman numeral is returned",{
  expect_match(object = convert_to_roman(4L), regexp = "^IV$")
})

test_that("passing an integer 5 the correct roman numeral is returned",{
  expect_match(object = convert_to_roman(5L), regexp = "^V$")
})

test_that("passing an integer between [6..8] the correct roman numeral is returned",{
  expect_match(object = convert_to_roman(6L), regexp = "^VI$")
  expect_match(object = convert_to_roman(7L), regexp = "^VII$")
  expect_match(object = convert_to_roman(8L), regexp = "^VIII$")
})

test_that("passing an integer 9 the correct roman numeral is returned",{
  expect_match(object = convert_to_roman(9L), regexp = "^IX$")
})

test_that("passing an integer 10 the correct roman numeral is returned",{
  expect_match(object = convert_to_roman(10L), regexp = "^X$")
})
#Simplest code possible to pass the tests
convert_to_roman <- function(num){
  if(!is.integer(num)) stop("must be integer")

  if(num == 1L) return("I")
  if(num == 2L) return("II")
  if(num == 3L) return("III")
  if(num == 4L) return("IV")
  if(num == 5L) return("V")
  if(num == 6L) return("VI")
  if(num == 7L) return("VII")
  if(num == 8L) return("VIII")
  if(num == 9L) return("IX")
  if(num == 10L) return("X")
}

Once we have a test harness around the code we can start to refactor the code in order to get a more general and flexible solution

convert_to_roman <- function(num){
  if(!is.integer(num)) stop("must be integer")

  roman_numeral <- NULL
  decimal_values <- c(10L, 9L, 5L,4L, 1L)
  roman_numerals <- c("X", "IX", "V","IV", "I")

  for (i in 1: length(decimal_values)){
    while(decimal_values[i] <= num){
      roman_numeral <- paste(roman_numeral, roman_numerals[i], sep = "")
      num <- num - decimal_values[i]
    }
  }
  return(roman_numeral)
}

Session Info

> sessionInfo()
R version 3.3.3 (2017-03-06)
Platform: x86_64-apple-darwin13.4.0 (64-bit)
Running under: macOS Sierra 10.12.6

locale:
[1] no_NO.UTF-8/no_NO.UTF-8/no_NO.UTF-8/C/no_NO.UTF-8/no_NO.UTF-8

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
[1] testthat_1.0.2   checkpoint_0.4.1

loaded via a namespace (and not attached):
[1] magrittr_1.5  R6_2.2.2      tools_3.3.3   crayon_1.3.2  digest_0.6.12