3. ● Depurando (print)
Temario
● Comprobación de inputs de una función (if / stop / assert)
● Manejando excepciones (try / try catch)
● Test unitarios
○ Características y ejemplos
○ Metodología TDD
4. Presentación a través de retos
En la exposición voy a ir presentando retos que resolver.
Cuando se alcance el resultado saldrá:
5. Primer reto
Crear una función, my_factorial(), que calcule el factorial de un
número.
El factorial de un entero positivo n se define como el producto de
los números enteros positivos desde 1 hasta n. Por ejemplo,
El factorial de 5 es:
1*2*3*4*5 = 120
6. Primera versión
my_factorial <- function(n) {
factorial <- 0
for(i in 1:n) {
factorial <- i * factorial
}
return(factorial)
}
> my_factorial(5)
[1] 0
✗
9. Version depurada
my_factorial <- function(n) {
factorial <- 1
for(i in 1:n) {
factorial <- i * factorial
}
return(factorial)
}
> my_factorial(5)
[1] 120
✓
10. Comprobación de inputs
> my_factorial(5)
[1] 120
> my_factorial(0)
[1] 0
> my_factorial(-5)
[1] 0
El factorial de un entero positivo n se define como el producto de
los números enteros positivos desde 1 hasta n. Por convenio el
factorial de 0 es 1.
✓
✗
✗
11. Segundo reto
Modificar la función, my_factorial(), para que en caso de un número
negativo devuelva un nulo y cuando se le pase 0 la solución sea 1.
12. Comprobación de inputs: if
my_factorial <- function(n) {
if(n < 0) {
return(NULL)
}
if(n == 0) {
return(1)
}
factorial <- 1
for(i in 1:n) {
factorial <- i * factorial
}
return(factorial)
}
> my_factorial(5)
[1] 120
> my_factorial(0)
[1] 1
> my_factorial(-5)
[1] NULL
> my_factorial("a")
Error in 1:n : NA/NaN argument
In addition: Warning message:
In my_factorial("a") : NAs
introduced by coercion
✓
✓
✓
13. Tercer reto
Pare la función my_factorial() cuando el valor de entrada no
sea numérico.
14. Comprobación de inputs: stop / if / assert
my_factorial <- function(n) {
stopifnot(is.numeric(n))
if(n < 0) {
return(NULL)
}
if(n == 0) {
return(1)
}
factorial <- 1
for(i in 1:n) {
factorial <- i * factorial
}
return(factorial)
}
> my_factorial("a")
Error: is.numeric(n) is not TRUE
✓ > my_factorial("a")
Error: Sorry, n must be a number
✓
my_factorial <- function(n) {
if(!is.numeric(n)){stop("Sorry, n must be a number")}
if(n < 0) {
return(NULL)
}
if(n == 0) {
return(1)
}
factorial <- 1
for(i in 1:n) {
factorial <- i * factorial
}
return(factorial)
}
15. Comprobación de inputs: stop / if / assert
library(assertthat)
my_factorial <- function(n) {
assert_that(
is.numeric(n),
msg = "Sorry, n must be a number"
)
if(n < 0) {
return(NULL)
}
if(n == 0) {
return(1)
}
factorial <- 1
for(i in 1:n) {
factorial <- i * factorial
}
return(factorial)
}
> my_factorial("a")
Error: Sorry, n must be a number
✓ ✓
library(assertthat)
my_factorial <- function(n) {
assert_that(is.numeric(n))
if(n < 0) {
return(NULL)
}
if(n == 0) {
return(1)
}
factorial <- 1
for(i in 1:n) {
factorial <- i * factorial
}
return(factorial)
}
> my_factorial("a")
Error: n is not a numeric or integer vector
16. Cuarto reto
Crear una función, my_factorial_for_file(), que calcule el
factorial de un número que lee de un archivo csv.
Y al final imprimir: “T ' F ! ”
17. Manejando excepciones
library(assertthat)
my_factorial_for_file <- function(filename) {
n <- read.csv(filename, header = FALSE)[1, 1]
assert_that(is.numeric(n))
if(n < 0) {
return(NULL)
}
if(n == 0) {
return(1)
}
factorial <- 1
for(i in 1:n) {
factorial <- i * factorial
}
return(factorial)
print("-- That's all folks! --")
}
> my_factorial_for_file("num.csv")
[1] 120
[1] "-- That's all folks! --"
> my_factorial_for_file("num2.csv")
Error in file(file, "rt") : cannot open
the connection In addition: Warning
message:
In file(file, "rt") :
cannot open file 'num2.csv': No such
file or directory
✓
✗
5
num.csv
18. Manejando excepciones: Try
library(assertthat)
my_factorial_for_file <- function(filename) {
try({
n <- read.csv(filename, header = FALSE)[1, 1]
assert_that(is.numeric(n))
if(n < 0) {
return(NULL)
}
if(n == 0) {
print(1)
}
factorial <- 1
for(i in 1:n) {
factorial <- i * factorial
}
print(factorial)
})
print("-- That's all folks! ---")
}
> my_factorial_for_file("num.csv")
[1] 120
[1] "-- That's all folks! --"
> my_factorial_for_file("num2.csv")
Error in file(file, "rt") : cannot
open the connection
In addition: Warning message:
In file(file, "rt") :
cannot open file 'num2.csv': No
such file or directory
[1] "-- That's all folks! ---"
✓
✓
5
num.csv
19. Manejando excepciones: Try catch
tryCatch({
expression to be evaluated
}, warning = function(w) {
warning-handler-code
}, error = function(e) {
error-handler-code
}, finally = {
cleanup-code
}
La sintaxis del manejo de excepciones de try catch en R es
similar a la de otros lenguajes:
20. Manejando excepciones: Try catch
library(assertthat)
my_factorial_for_file <- function(filename) {
tryCatch({
n <- read.csv(filename, header = FALSE)[1, 1]
assert_that(is.numeric(n))
if(n < 0) {
return(NULL)
}
if(n == 0) {
print(1)
}
factorial <- 1
for(i in 1:n) {
factorial <- i * factorial
}
print(factorial)
}, warning = function(w) {
print("Ups, a warning:")
print(w)
}, error = function(e) {
print("There is an error:")
print(e)
}, finally = {
print("-- That's all folks! ---")
})
}
> my_factorial_for_file("num.csv")
[1] 120
[1] "-- That's all folks! --"
> my_factorial_for_file("num2.csv")
[1] "Ups, a warning:"
<simpleWarning in file(file, "rt"):
cannot open file 'num2.csv': No such
file or directory>
[1] "-- That's all folks! ---"
✓
✓
5
num.csv
21. Comprobaciones una y otra vez sobre lo mismo
> my_factorial(5)
[1] 120
> my_factorial(0)
[1] 1
> my_factorial(-5)
[1] NULL
23. Test unitarios
● Solo deben fallar cuando se introduce un bug
● Los test deben acabar rápidamente, tardar menos de un minuto en ejecutarse
● Debe ser fácil detectar dónde se produce el error para poderlo solucionar
Un test unitario es una secuencia de comandos que evalúa la unidad más pequeña del código
y lo compara con el comportamiento esperado.
Deberían:
● Los test deben ser independientes unos de otros
27. "testthat: Get Started with Testing" Hadley Wickham
Expectation
Una expectation es una afirmación binaria sobre si un valor es o no lo que se
espera. Sólo en caso de no ser verdadera dará un error.
Hay 11 tipos:
expect_that(x, is_true())
expect_that(x, is_false())
expect_that(x, is_a(y))
expect_that(x, equals(y))
expect_that(x, is_equivalent_to(y))
expect_that(x, is_identical_to(y))
expect_that(x, matches(y))
expect_that(x, prints_text(y))
expect_that(x, shows_message(y))
expect_that(x, gives_warning(y))
expect_that(x, throws_error(y))
expect_true(x)
expect_false(x)
expect_is(x, y)
expect_equal(x, y)
expect_equivalent(x, y)
expect_identical(x, y)
expect_matches(x, y)
expect_output(x, y)
expect_message(x, y)
expect_warning(x, y)
expect_error(x, y)
Full Short curt
28. Ejemplos de expectations
library("testthat")
test_that("example of expectations", {
expect_equal(1, 1) # pass
expect_equal(1, 1 + 1e-8) # pass
expect_equal(1, 5) # fail
expect_identical(1, 1) # pass
expect_identical(1, 1 + 1e-8) # fail
expect_identical(3, 1 + 2) # pass
expect_identical(0.3, 0.1 + 0.2) # fail
})
> test_file('tests/test.examples.R')
..1.2.3
Failed
---------------------------------------------
1. Failure: example of
expectations(@test.examples.R#6) ------------
1 not equal to 5.
1/1 mismatches
[1] 1 - 5 == -4
2. Failure: example of expectations
(@test.examples.R#8) ------------------------
1 not identical to 1 + 1e-08.
Objects equal but not identical
3. Failure: example of expectations
(@test.examples.R#10) -----------------------
0.3 not identical to 0.1 + 0.2.
Objects equal but not identical
DONE ========================================
1
2
3
4
5
6
7
8
9
10
11
30. asserthat::with_mock()
with_mock() ejecuta el código después de sustituir temporalmente las implementaciones de
las funciones del paquete.
Podemos tener en nuestro código funciones que sean difícil de testear.
● Llamadas a un servicio que no está disponible
● Resultados impredecibles al llamar a una función
● Un código que tarde demasiado en ejecutarse
¿Cómo testear en estos casos que el código es el correcto?
33. Último reto
Crear una función, add_one(), que sume uno al número que se le pase.
En caso de que no se le pase un número debe devolver NULL.
Utilizar la metodología Test-Driven Development (TDD).
34. Workflow (basado en Test-driven development)
Escribe el test
tests/test.foo.R
Escribe el código
mínimo
foo.R
¿Pasa
el
test?
No
36. Por qué escribir el test antes que el código
● Nos aseguramos de obtener el resultado deseado y de que
efectivamente vamos a generar código testeable
● Sirve de guía si no sabes por dónde empezar pero sabes lo que
quieres obtener
● Seamos realistas, lo más seguro es que una vez que el código
realiza lo que queremos nos dará más pereza escribir el test
37. Test unitarios y paquetes: Test workflow
● Crear un paquete
● Crear test para una función, add_one(), que sume una unidad al número que
se le pase de argumento y en caso de no pasarle un número nos devuelva un
texto
● Crear la función, add_one()
● Estudiar la cobertura de test del paquete
49. Extra: traceback()
> g <- function(x) x + "a"
> g(0)
Error in x + "a" : non-numeric argument to binary operator
> f <- function(x) g(x)
> f(0)
Error in x + "a" : non-numeric argument to binary operator
> traceback()
2: g(x) at #1
1: f(0)