A quick intro to Property-Based Testing

2020-12-08

With Elixir, PropCheck and the Fibonacci series

First off, what’s Property-Based Testing? to describe it in a phrase that’s simple, easy to understand and wrong, “it’s a method by which you automatically generate automated tests that check for a given rule”. It is kind of like that, but the most important part of PBT doesn’t lie in the code itself but in the mindset you have to adopt to make full use of it: thinking in terms of properties instead of tests is key, but it’s not something that can be taught in a simple tutorial such as this. So instead, my aim is to just show it a little, and see if you’re interested enough to study it on your own.

The sample project

We’ll do a simple library in Elixir that implements a well-known algorithm: the Fibonacci series.

We start with creating the project:

$ mix new fib

Then we add in mix.exs our dependency, PropCheck:

defp deps do
  [
    {:propcheck, "~> 1.3", only: [:test, :dev]}
  ]
end

And of course, we add our tail-optimized implementation:

def fib(n), do: f(n, 0, 1)

defp f(n, a, b)
defp f(0, a, _b), do: a
defp f(n, a, b), do: f(n - 1, a + b, a)

Now, you’d be tempted to write some traditional tests to make sure this works—the code does look a bit messy, no? so let’s go ahead and add them:

describe "traditional tests" do
  test "validate the initial elements in the series" do
    assert Fib.fib(1) == 1
    assert Fib.fib(2) == 1
    assert Fib.fib(3) == 2
    assert Fib.fib(4) == 3
    assert Fib.fib(5) == 5
    assert Fib.fib(6) == 8
  end
end

A bit wordy, but they validate just fine, and will give us some peace of mind as we move on to testing properties instead.

Our first property

The beauty of mathematical functions is that properties are so easy to find. And what’s the easiest property we can imagine for the Fibonacci series? that for all $n > 0, Fib(n) > 0$. So first we add PropCheck into our code:

use PropCheck, default_opts: [numtests: 100]

The numtest variable already hints at why Property-Based Testing is so cool: with just the coding of a single property, we can get a hundred tests for “free”. So let’s go ahead and do that:

describe "the fibonacci series" do    
  property "is composed only of positive integers for n > 0" do
    forall n <- non_neg_integer() do
      num = Fib.fib(n)

      is_integer(num) and num > 0
    end
  end
end

So we run it with mix test and… oops.

Property Elixir.FibTest.property the fibonacci series is composed only of non-zero integers() failed. Counter-Example is:
     [0]

Of course, we forgot to make sure our n variable wasn’t zero and $Fib(0)=0$, so we make two simple changes: add our own custom generator, and rewrite the property to use it:

defp positive_int, do: such_that num <- non_neg_integer(), when: num > 0

describe "the fibonacci series" do    
  property "is composed only of positive integers for n > 0" do
    forall n <- positive_int() do
      num = Fib.fib(n)

      is_integer(num) and num > 0
    end
  end
end

And…

$ mix test
....................................................................................................
OK: Passed 100 test(s).

Great!

Now, the part about writing our own custom generators might sound scary at first, but as you can see it’s just a bit of metaprogramming magic that makes it easy to extend PropCheck’s own generators and as you can see, it flows quite naturally once you learn the basic syntax.

The model

Now we’ll implement another kind of property: the model. Which is, simply, ensure that our production-grade implementation works the same as a slower, but more obviously correct reference implementation. So let’s try that, first by writing a straight implementation of the definition for the Fibonnaci series as a test helper:

defp model_fib(n) do
  if n <= 2 do
    1
  else
    model_fib(n-1) + model_fib(n-2)
  end
end

And then our property comparing both their results:

property "works the same as the reference implementation for n in (1, 30)" do
  forall n <- integer(1, 30) do
    num = Fib.fib(n)
    ref = model_fib(n)

    num == ref
  end
end

OK, I admit it: I cheated. Not because of any problem with my implementation, but the reference one: it’s extremely slow for integers as small as $n=100$, so I forced it to choose a number between 1 and 30 instead of reusing our generator. And…

$ mix test
.....................................................................................................
OK: Passed 100 test(s).
.....................................................................................................
OK: Passed 100 test(s).
.

Finished in 0.6 seconds
2 properties, 1 test, 0 failures

Sweet!

Refactoring

This model-based Property-Based Testing is particularly useful for TDD, since you can take your initial implementation as the model, then optimize to your heart’s content with a really strong regression suite backing it up; no matter how crazy your idea, your tests ensure your implementation remains consistent, among a wider variety of inputs than with tests written by hand.

It happened while I was writing this article, in fact, when I thought of implementing the Fibonacci series using Streams. Streams are a feature present in many languages (such as Java) where you construct a List-like object using a lazily-evaluated generator function; usually to avoid having the entire list in memory, but they can also be used to implement infinite series like Fibonacci.

So, first we use our tail-recursive implementation, whose correctness we’ve already established, as the new model:

defp model_fib(n), do: f(n, 0, 1)

defp f(n, a, b)
defp f(0, a, _b), do: a
defp f(n, a, b), do: f(n - 1, a + b, a)

This allows us to remove our “cheat” from last section, and test the entirety of positive integers again:

property "works the same as the reference implementation" do
  forall n <- positive_int() do
    num = Fib.fib(n)
    ref = model_fib(n)

    num == ref
  end
end

And finally, our Fibonacci implementation using Streams:

@seed {0, 1}

def stream do
  @seed
  |> Stream.iterate(fn {a, b} -> {a + b, a} end)
  |> Stream.map(fn {a, _b} -> a end)
end

def fib(n) do
  stream()
  |> Enum.at(n)
end

Much like before, we check that our properties are still fulfilled:

$ mix test
.....................................................................................................
OK: Passed 100 test(s).
.....................................................................................................
OK: Passed 100 test(s).
.

Finished in 0.2 seconds
2 properties, 1 test, 0 failures

Even though our Stream implementation is short, I wouldn’t be surprised if some were confused by it; they work quite differently from other enumerable types like List and Map that one may be accustomed to. However, thanks to our property-based test suite, we know it’s not only correct, but highly performant as well.

Conclusion

This is a really quick introduction to Property-Based Testing, but hopefully you can now see why I like it, and maybe even motivate you enough to study it in depth yourself. With only this little code, we already have hundreds of tests checking the validity of our small library, checking for data we likely wouldn’t have thought to check with traditional tests.

To study Property-Based Testing, I recommend Fred Hebert’s excellent book, Property-Based Testing with PropEr, Erlang, and Elixir), also available over at PragProg.

Also, if you want, you can check out the code for the samples used in this post over at my GitHub.

¿Por qué Elixir?

2020-12-05

Las razones por las que decidí especializarme en Elixir

Razones para usar un lenguaje de programación determinado son tan diversas como lenguajes en sí, pero una pregunta que recibo a menudo es, ¿por qué Elixir? aquí, intentaré delinear mis razones personales por las que decidí especializarme en él.

1. Es limpio

Desde que comencé a programar hace veinte años, la legibilidad ha sido mi principal preocupación—con los lenguajes que uso, el código que escribo, y las arquitecturas que diseño. Tampoco es una preocupación meramente estética; como Eric S. Raymond famosamente dijera,

Los programas feos son como puentes feos: tienen un riesgo mucho mayor de caer que los bonitos, porque la forma en que humanos (especialmente ingenieros) perciben la belleza está intimamente relacionada con la forma en que procesamos y comprendemos la complejidad. Un lenguaje que dificulta el escribir código elegante dificulta el escribir buen código.

— Eric S. Raymond, Why Python? (traducción propia)

Y en mi experiencia, ningún lenguaje es mejor en permitir crear código bello que Elixir. En gran medida ésto es porque deriva mucho tanto de la sintáxis bella de Ruby como el diseño elegante de Erlang, pero también tiene constructos interesantemente elegantes sobre ellos, como la sentenciawith:

def get_post(category, date, id) do
  with {:ok, date} <- Date.from_iso8601(date),
       {:ok, result} <- Xandra.execute(conn, get_query, [category, date, id]),
       {:ok, post} <- parse_post(result) do
    post
  end
end

Con ella y la comparación de patrones de Erlang, podemos lidiar elegantemente con el hecho que aceptamos un parámetro como String cuando nuestra librería espera una estructura Date. Pero, ¿qué sucede si alguno de los patrones falla? la expresión completa hace corto-circuito y retorna la tupla “problemática”, casi seguro de la forma {:error, reason}, la que es luego retornada por la función; todo de forma implícita, sin tener que lidiar manualmente con el error.

2. Es flexible con distintas filosofías

Otra razón por la que me gusta Elixir es porque siento que puedo escribir código en una variedad de estilos y filosofías distintas, sin perder mucho de su elegancia. ¿Han intentado alguna vez escribir LISP de forma procedural? ¿Código Orientado a Objetos en C puro? ¿Java en el estilo Funcional? es un ejercicio divertido y educativo, pero también deja claro por qué sería una decisión terrible para la mayoría de proyectos reales.

Elixir no es adecuado para todos los paradigmas posibles, tampoco, pero encuentro que es mucho más tolerante de pequeños faux pas ocasionales que otros lenguajes—y correspondientemente, que no fuerza sus propios paradigmas tanto, como el Modelo de Actores que forma la base detrás de GenServer y abstracciones superiores como Agent.

Pero incluso dejando de lado los grandes paradigmas, también es flexible en decisiones pequeñas como cuan estrictos ser con el chequeo de tipado. Por ejemplo, en el código de ejemplo de la sección anterior, podríamos haber hecho explícito que nuestros parámetros eran de tipo String agregando antes la siguiente línea:

@spec get_post(String.t, String.t, String.t) :: Post.t | {:error, String.t}

Algunos se sentirán inclinados, entonces, a usar specs en cada función que escriban y usar herramientas de análisis estático como credo para chequear su validez. Otros encontrarán que todo el tema es una molestia y los omitirán consistentemente, mientras que un tercer grupo puede, como yo, decidir usarlos solo en funciones donde los valores que espera o retorna no sean inmediatamente aparentes. Y Elixir soporta todas estas filosofías igualmente bien, sin sentir en algún punto que luchas contra el lenguaje para hacerlo.

3. Tiene un ecosistema fecundo

Para personas que vienen de lenguajes más populares como Ruby o Javascript, ésto sonará extraño a primeras. Pero para aquellos que nos gusta jugar con lenguajes más pequeños—comencé con Elixir hace cinco años ya—conveniencias tales como soporte de CI, bibliotecas para trabajar con bases de datos no-relacionales como Scylla, o incluso un número de usuarios suficientes para depurar un problema con Docker o Kubernetes sin entrar a ver el código fuente de las bibliotecas estándar son comunmente el precio que pagamos por las innovaciones que buscamos.

Eso no significa que haya dejado de usar Crystal u otros lenguajes más pequeños, por supuesto. Pero el punto aquí es uno de suficiencia: siento confianza en que al decidir especializarme en Elixir, seré capaz de usarlo en la mayoría de mis proyectos tanto personales como profesionales, sin necesitar volver a un lenguaje más tradicional de forma tan frecuente que pierda el punto de la especialización.

¿Otras razones?

Hay muchas más ventajas de Elixir, pero una ausencia notable de la lista previa es rendimiento. No porque Elixir rinda mal—benchmarks en distintos sitios muestran lo opuesto, de hecho. Pero he llegado a pensar que hay pocos problemas donde el rendimiento del lenguaje de programación mismo se convierta en el cuello de botella; usualmente entre novatos el problema está en la interfaz entre las capas de aplicación y persistencia, mientras que entre programadores más experimentados (y aquí me incluyo) tiende a ser en la optimización de la capa de persistencia misma. Incluso Ruby, infame por su pobre rendimiento, corre bastante bien y con uso de memoria limitado cuando la solución tiene una buena arquitectura, y se acopla a una base de datos bien indexada y bien consultada detrás. Pero ese es tema para otra oportunidad.

Nuevo blog, reescrito y rediseñado

2020-12-04

¿Por qué arreglar lo que no está roto, si puedes romperlo y rehacerlo desde cero?

Luego de más tiempo del que quisiera admitir, finalmente reescribí el software completo detrás del blog; pasé de Crystal a Elixir, de Redis/KeyDB a Cassandra/Scylla, de Bulma.css a Spectre.css, y de una API JSON RESTful a una GraphQL. El código está abierto en mi cuenta de BitBucket, aunque probablemente lo migre pronto a GitHub para usar su sistema de CI; el de BitBucket aún no soporta Elixir y me pone un poco nervioso luego de tantos meses trabajando con pipelines CI bien configuradas. Por supuesto, ya encontré un par de bugs abiertos con los tasks del deployment, pero por eso esa branch sigue abierta—técnicamente lo que puse en producción es un pre-release.

El principal cambio fuera de las tecnologías es la posibilidad de categorización; no solo agregaré allí discusiones tecnológicas sino también sobre Filosofía y relacionados, como Matemática y Teoría Política; principalmente sacados desde mi cuenta de Facebook y pulidos, pero puede escriba contenido original también.

Una de mis metas principales también era usar el proyecto para lucir lo que he estado estudiando en Elixir, como Property-Based Testing o el patrón CQRS/ES, pero en realidad no hay mucho espacio para algo así en un simple blog de solo-lectura. Con suerte podré integrar algunos tests con hound y Selenium, quizás, cuando migre a GitHub e integre una pipeline de CI; asumiendo las imágenes que usan tengan disponible un navegador para los planes gratuitos, que dudo.

¿Quizás debiese hacer un GitLab self-hosted? Ahhh, no, ya pasé una vez por el tener repositorios de Git self-hosted. No fue tan malo, hasta que la empresa decidió integrar una interfaz web, ugh. Además que estoy seguro no corre en los 512 MBs de RAM del VPS, ciertamente no compartiendo espacio con Postgres y Scylla.

Pero bueno, nuevo blog. Same as the old one, mostly.