Skip to end of metadata
Go to start of metadata

You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 6 Next »

Test-driven paradigm (TDD)

This software development process relies on some well-defined requirements to be met. The latter becomes specific test cases. Each test case must be met at any time of the software life; that is if any code is altered, the tests must continue to be met. Traditionally, we write a test first from the lists of test cases and expect the test to fails; the latter is an expected outcome as the no code is written yet. The third step is to write the code and run the test again. These two steps may be repeated until the test passes. Once the test passes, the code is refactored; this phase organises the code again, so that it becomes highly maintainable (see diagram below).

Refactoring

The last phase of the (TDD) development cycle requires to structure again the code, without changing its behaviour. In R, this step often requires shorten lengthy functions into a more manageable and maintainable size.

To achieve this purpose, code is broken down into reusable functions that present clear, well-defined, simple-to-use arguments. Each of these functions should be named meaningfully and complete only one goal. Those are referred as helper functions.

The benefits of refactoring brings some R functions that can be maintained more easily. The reduced size of the code should enhances comprehension, through self-documentation of the logical flow of the implementation and meaningful naming of variables and helper functions.

“Whenever I have to think to understand what the code is doing, I ask myself if I can refactor the code to make that understanding more immediately apparent.”
Martin Fowler, Refactoring: Improving the Design of Existing Code

Example of refactored code:

This refactored function getOpals uses three helper functions: init.object.list.testing.environment, init.object.list.global.environment and init.opal.list. Each of these helper function complete a unique aim; the logic of the function becomes easier to read as the body of the selection now holds only one line. Each of these helper functions have a certain level of complexity (i.e., use of iterations and selections), that would be challenging to understand in the getOpals function. Also, the list returned is build using some values.

The getOpals function

getOpals <- function()

{
  
  # Declare function variable
  flag        <- 0
  
  opal.list   <- NULL
  
  return.list <- list("flag"=flag, "opals"=NULL, "opals.list"= NULL)

  
  curr.ds.test_env <- NULL
  
  curr.ds.test_env <- get("ds.test_env", envir = .GlobalEnv)

  
  
  # Check the computer environment used to execute the code. Then initialise
  # accordingly the variable opal.list
  if (! is.null(curr.ds.test_env))
  
  {
    
    opal.list <- init.object.list.testing.environment(ls(curr.ds.test_env))
  
  }
  
  else
 
  {
    
    opal.list <- init.object.list.global.environment(ls(.GlobalEnv))
  
  }
  
  
  return.list <- init.opal.list(opal.list)
  
return(return.list)
}

helper function no 1

init.object.list.global.environment <- function(objs)
{
  opalist <- vector('list')
  counter <- 0
  for(i in 1:length(objs))
  {
    class.element.name = class(eval(parse(text=objs[i])))
    if(class.element.name[1] == 'list')
    {
      list2check <- eval(parse(text=objs[i]))
      if(length(list2check) > 0)
      {
        cl2 <- class(list2check[[1]])
        for(s in 1:length(cl2))
        {
          if(cl2[s] == 'opal')
          {
            counter <- counter + 1
            opalist[[counter]] <- objs[i]
          }
        }
      }
    }
  }
  return(opalist)
}

helper function no 2

init.object.list.testing.environment <- function(objs)
{
  opalist <- vector('list')
  counter <- 0 
  for(i in 1:length(objs))
  {
     if (objs[i] == "connection.opal")
     {
        counter <- counter + 1
        opalist[[counter]] <- paste("ds.test_env$",objs[1],sep="")
     }
  }
  return(opalist)
}

helper function no 3

init.opal.list <- function(opal.list)
{
     if(length(opal.list) > 1)
     {
        flag <- 2
        return(list("flag"=flag, "opals"=unlist(opal.list), "opals.list"=unlist(opal.list)))
     }
     else
     {
        flag <- 1
        return(list("flag"=flag, "opals"=eval(parse(text=opal.list[[1]])), "opals.list"=unlist(opal.list)))
     }
 }

Additional reading:

Unit testing

Unit testing is often related to test-driven development. The tests are broken into units; each of them ensure that a section of an application meets its design and behaves as intended. For example, if the division operation would be implemented as an R function, then the following designed would be valid:

Name of the function:

divide

Description:

implements the divide arithmetical operation using two numbers.

Arguments:

numerator - number to be divided

denominator- number dividing the numerator

Returned value:

The quotient - a numerical value

Test cases or units:

  1. denominator x quotient = numerator

  2. numerator / quotient = denominator

  3. Division by 0 shows an error message

  4. Character arguments show an error message

Testthat Package

In R, testthat package provides a unit testing framework. This package has been integrated in the DataSHIELD test framework.

  • No labels