Mutation Testing

Las pruebas sirven para determinar si nuestro código es o no funcional. Sin embargo no es trivial poder determinar el grado en el que nuestro código está cubierto con pruebas, para ello tenemos herramientas como simplecov, pero dicha herramienta maneja una métrica basada en las llamadas a cada línea de código, más que nada como una métrica de cantidad, ¿es suficiente?

Miquel Barceló, Dore, 2013

¿Para qué sirve?

Sirve para determinar la calidad de nuestras pruebas y la cobertura de las mismas para nuestro código.

¿Qué es?

El concepto de mutation testing es simple, se emplean faults (mutations) en el código de forma automática y después se ejecutan las pruebas. Si las pruebas fallan se eliminan las respectivas mutaciones y si las pruebas pasan las mutaciones viven, entonces se determina que en ese segmento de código hay un caso que no está cubierto.

La calidad de las pruebas se relaciona con el porcentaje de mutaciones eliminadas.

Ejemplo

Suponemos el siguiente código lib/polygon.rb

class Side < Struct.new(:number, :length); end
class Polygon
attr_reader :sides
  def initialize
@sides = []
@index = {}
end
  def side(number)
@index.fetch(number) {
raise "Polygon doesn't have a side with number: #{number}"
}
end
  def add_side(side)
@sides << side
@index[side.number] = side
self
end
end

Y spec/lib/polygon_spec.rb

describe Polygon, '#add_side' do
subject(:polygon) { Polygon.new }
let(:side) { Side.new(1) }
  it 'should return a polygon' do
expect(polygon.add_side(side)).to be(polygon)
end
  it 'should add a side to polygon' do
polygon.add_side(side)
expect(polygon.sides).to include(side)
end
end

Claramente no tenemos una prueba para el hash @index, y nuestra herramienta simplecov nos marca el 100%, vamos a probar con mutation testing

$ mutant -I lib -r polygon '::Polygon#add_side' --rspec-dm2
Mutant configuration:
Matcher: #<Mutant::Matcher::Method::Instance identification="Polygon#add_side">
Filter: Mutant::Mutation::Filter::ALL
Strategy: #<Mutant::Strategy::Rspec::DM2>
Subject: Polygon#add_side:/home/david/dump/polygon/lib/polygon.rb:17
# ...
!!! Mutant alive: rspec:Polygon#add_side:/home/david/dump/polygon/lib/polygon.rb:17:3cc34 !!!
@@ -1,6 +0,5 @@
def add_side(side)
((@sides) << (side))
- @index[side.number] = side
self
end
Took: (0.05s)
subjects:   1
mutations: 8
noop_fails: 0
kills: 7
alive: 1
mtime: 0.04s
rtime: 0.18s

Y como puede observarse hay una mutación viva, es decir la herramienta modificó nuestro código (quitó la linea @index[side.number] = side) y efectuó la prueba, sin embargo, está siguió pasando, de manera que dicha prueba no es óptima.

En Ruby

Puedes utilizar Mutant, que es compatible con Ruby 2.1>=, hasta ahora no tiene una documentación muy accesible, pero en el repositorio encontrarás diversas publicaciones que podrán ayudarte.

Conclusiones

  • Las herramientas de cobertura no garantizan pruebas óptimas aunque marquen el 100%.
  • Mutation testing te ayudará a encontrar debilidades en tus pruebas.

Bibliografía

Contacto

Giovanni Alberto García

delirable@gmail.com

@yovasx2