Permalink
Browse files

Update mix-proper to AsciiDoc

  • Loading branch information...
1 parent 11e7d18 commit 447d0eaa1d993211810edad72f089ad7a32754ab @jeffkreeftmeijer committed Oct 22, 2017
Showing with 445 additions and 69 deletions.
  1. +1 −0 Rakefile
  2. +1 −1 _articles/mix-proper
  3. +273 −0 _output/mix-proper/amp.html
  4. +170 −68 _output/mix-proper/index.html
View
@@ -4,6 +4,7 @@ task :generate do
{
'_articles/git-flow/git-flow.adoc' => '_output/git-flow/',
'_articles/vim-number/vim-number.adoc' => '_output/vim-number/',
+ '_articles/mix-proper/mix-proper.adoc' => '_output/mix-proper/',
'_articles/open-source-maintainability/index.adoc' => '_output/open-source-maintainability/',
'_articles/vim-reformat-dates/vim-reformat-dates.adoc' => '_output/vim-reformat-dates/',
}.each do |from, to|
View
@@ -0,0 +1,273 @@
+<!DOCTYPE html>
+<html amp lang="en">
+<head>
+<meta charset="utf-8"/>
+<title>Property-based testing in Elixir using PropEr</title>
+<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
+<meta name="author" content="Jeff Kreeftmeijer">
+<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgo=">
+<link rel="alternate" type="application/atom+xml" href="/feed.xml">
+<link rel="canonical" href="https://jeffkreeftmeijer.com/mix-proper/">
+<script async src="https://cdn.ampproject.org/v0.js"></script>
+<script async custom-element="amp-video" src="https://cdn.ampproject.org/v0/amp-video-0.1.js"></script>
+<script async custom-element="amp-analytics" src="https://cdn.ampproject.org/v0/amp-analytics-0.1.js"></script>
+<style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>
+<style amp-custom>body,pre{padding:1em}body{font:18px/1.4 system-ui,sans-serif;margin:auto;max-width:40em}h1,h2,h3,h4,h5,h6{line-height:normal}img,video{max-width:100%;height:auto}@media (max-width:375px){body{font-size:.9em}}@media (max-width:320px){body{font-size:.8em}}code,kbd,pre{font:.8em/1.4 SF Mono,Monaco,monospace;background-color:#f8f8ff}code,kbd{padding:.2em .4em}pre code{font:inherit;padding:0}pre{overflow-x:auto}blockquote{font-style:italic}td{padding:.5em}aside.ad{position:absolute;top:2.34em;left:50%;width:50%;overflow:hidden;z-index:-100}aside.ad div{margin-left:22em;width:130px}aside.ad a{text-decoration:none;color:#708090;font-size:.7em;display:block;margin-bottom:1em}.carbon-poweredby{font-style:italic}
+article aside{border: 1px solid lightgrey;padding:0 1em}nav{display:flex;justify-content:space-between}</style>
+
+<meta name="twitter:card" content="summary_large_image">
+<meta name="twitter:site" content="@jkreeftmeijer">
+<meta name="twitter:creator" content="@jkreeftmeijer">
+<meta name="twitter:title" content="Property-based testing in Elixir using PropEr">
+<meta name="twitter:description" content="">
+<meta name="twitter:image" content="https://jeffkreeftmeijer.com/mix-proper/mix_proper.png">
+
+<script type="application/ld+json">
+{
+ "@context": "http://schema.org",
+ "@type": "Article",
+ "mainEntityOfPage": {
+ "@type": "WebPage",
+ "@id": "https://jeffkreeftmeijer.com/mix-proper/"
+ },
+ "headline": "Property-based testing in Elixir using PropEr",
+ "image": ["https://jeffkreeftmeijer.com/mix-proper/mix_proper.png"],
+ "datePublished": "2017-08-22",
+ "dateModified": "2017-10-22T04:44:14Z",
+ "author": {
+ "@type": "Person",
+ "name": "Jeff Kreeftmeijer"
+ },
+ "description": "",
+ "publisher": {
+ "@type": "Organization",
+ "name": "jeffkreeftmeijer.com",
+ "logo": {
+ "@type": "ImageObject",
+ "url": "https://jeffkreeftmeijer.com/logo.png"
+ }
+ }
+}
+</script>
+</head>
+<body>
+<h1>Property-based testing in Elixir using PropEr</h1>
+By <a href="/" rel="author">Jeff Kreeftmeijer</a>
+
+<article>
+<div id="preamble">
+<div class="sectionbody">
+<div class="paragraph">
+<p>While reading Fred Hébert&#8217;s <a href="https://propertesting.com">PropEr testing</a>, on
+property-based testing in Erlang, I set out write
+<a href="http://proper.softlab.ntua.gr:">PropEr</a> tests in Elixir, and run them with a Mix
+task.</p>
+</div>
+<div class="imageblock">
+<div class="content">
+<amp-img layout="responsive" src="https://jeffkreeftmeijer.com/mix-proper/mix_proper.png" alt="mix_proper running property-based tests in an Elixir project" width="1321" height="719">
+</div>
+</div>
+</div>
+</div>
+<div class="sect1">
+<h2 id="writing-proper-tests-in-elixir">Writing PropEr tests in Elixir</h2>
+<div class="sectionbody">
+<div class="paragraph">
+<p>To explain writing PropEr tests (named &#8220;properties&#8221;) in Elixir, let&#8217;s take
+<a href="http://propertesting.com/book_stateless_properties.html#_writing_properties:">an
+example from PropEr testing</a>. A property of a function named <code>biggest/1</code> is
+that the returned value is equal to the biggest value in the passed list.</p>
+</div>
+
+<pre class="highlight"><code class="erlang language-erlang"># test/prop_biggest.erl
+-module(prop_biggest).
+-include_lib("proper/include/proper.hrl").
+
+prop_biggest() -&gt;
+ ?FORALL(List, non_empty(list(integer())), # ➊
+ begin
+ biggest(List) =:= lists:last(lists:sort(List)) # ➋
+ end).</code></pre>
+<div class="colist arabic">
+<ol>
+<li>
+<p><code>?FORALL</code> is a macro around <code>proper:forall/2</code>. It takes a variable name, a generator, and a property. In this case, the generator returns a random, non-empty list of integers and puts it in the <code>List</code> variable for every run.</p>
+</li>
+<li>
+<p>The property itself is a function that uses the random variable to assert the result from <code>biggest/1</code> matches the biggest value in the list.</p>
+</li>
+</ol>
+</div>
+<div class="paragraph">
+<p>Converting it to Elixir, this property looks a bit different.</p>
+</div>
+
+<pre class="highlight"><code class="elixir language-elixir"># test/biggest_prop.exs
+defmodule BiggestProp do
+ # ➊
+ import :proper
+ import :proper_types
+
+ def prop_biggest do
+ forall(non_empty(list(integer())), fn(list) -&gt; # ➋
+ biggest(list) == list |&gt; Enum.sort |&gt; List.last # ➌
+ end)
+ end
+
+ # ...
+end</code></pre>
+<div class="colist arabic">
+<ol>
+<li>
+<p>The <code>BiggestProp</code> module imports <code>:proper</code> and <code>:proper_types</code> manually instead of including the <code>proper.hrl</code> header file.</p>
+</li>
+<li>
+<p>Elixir <a href="https://groups.google.com/forum/#!topic/elixir-lang-talk/VbGTz7rKebM:">doesn&#8217;t support Erlang&#8217;s macros</a>, so it calls <code>:proper.forall/2</code> directly.</p>
+</li>
+<li>
+<p>Although Erlang&#8217;s <code>lists</code> module would work, it uses <code>Enum.sort/1</code> and <code>List.last/1</code> as trusted functions to test the implementation.</p>
+</li>
+</ol>
+</div>
+<div class="paragraph">
+<p>With a
+<a href="https://github.com/jeffkreeftmeijer/mix_proper_example/blob/a09d6ac1bc800ae3f77a105c76f8db44d9b8d5ce/test/biggest_prop.exs#L19-L27:">working
+implementation</a>, and <a href="https://hex.pm/packages/proper:">:proper</a> included in the
+project&#8217;s dependencies, the property runs in IEx by requiring the test file
+manually and running the property through <code>:proper.quickcheck/1</code>.</p>
+</div>
+
+<pre>$ iex -S mix
+iex(1)&gt; Kernel.ParallelRequire.files(["test/biggest_prop.exs"])
+[BiggestProp]
+iex(2)&gt; :proper.quickcheck(BiggestProp.prop_biggest())
+....................................................................................................
+OK: Passed 100 test(s).
+true</pre>
+</div>
+</div>
+<div class="sect1">
+<h2 id="rebar3_proper-and-mix_proper">rebar3_proper and mix_proper</h2>
+<div class="sectionbody">
+<div class="paragraph">
+<p>Although that worked, it would be nice to have a command to run all tests in a
+Mix project. For Erlang, there&#8217;s a library named
+<a href="https://github.com/ferd/rebar3_proper:">rebar3_proper</a> that does just that by
+adding the <code>rebar3 proper</code> command.</p>
+</div>
+
+<pre>$ rebar3 proper
+===&gt; Testing prop_biggest:prop_biggest()
+....................................................................................................
+OK: Passed 100 test(s).
+===&gt;
+1/1 properties passed</pre>
+<div class="paragraph">
+<p>rebar3_proper runs tests by finding functions and modules with names prefixed
+with "prop_" and running them through <code>proper:quickcheck/1</code>. That would work in
+Elixir by adding a Mix task that uses the same approach.</p>
+</div>
+
+<pre class="highlight"><code class="elixir language-elixir">defmodule Mix.Tasks.Proper do
+ use Mix.Task
+
+ def run([]) do
+ "test/**/*_prop.exs"
+ |&gt; Path.wildcard
+ |&gt; Kernel.ParallelRequire.files # =&gt; [BiggestProp]
+ # ...
+ end
+
+ # ...
+end</code></pre>
+<div class="paragraph">
+<p>Using &#8220;test/**/*_prop.exs&#8221; as a wildcard pattern, the task finds the paths
+for all property-based testing files in the test directory. Being .exs files,
+they are not compiled, so they&#8217;re required manually when they&#8217;re needed.
+<a href="https://github.com/elixir-lang/elixir/blob/df7e0ca55cd03e3d46f426c7cd02fd25dcf2df87/lib/mix/lib/mix/compilers/test.ex#L50:">Mix&#8217;s
+own test task uses <code>Kernel.ParallelRequire.files/1-2</code></a>, which takes a list of
+filenames, includes the modules in the files and returns the names of the newly
+included modules.</p>
+</div>
+
+<pre class="highlight"><code class="elixir language-elixir">defmodule Mix.Tasks.Proper do
+ use Mix.Task
+
+ def run([]) do
+ "test/**/*_prop.exs"
+ |&gt; Path.wildcard
+ |&gt; Kernel.ParallelRequire.files
+ |&gt; Enum.each(fn(module) -&gt;
+ module.__info__(:functions)
+ |&gt; Enum.filter(&amp;property?/1)
+ |&gt; Enum.map(fn({name, 0}) -&gt;
+ :proper.quickcheck(apply(module, name, []))
+ end)
+ end)
+ end
+
+ # ...
+end</code></pre>
+<div class="paragraph">
+<p>The task uses the list of included modules to find their properties using the
+<code>__info__/1</code> function. Functions without the proper name or an arity other
+than 0 get
+<a href="https://github.com/jeffkreeftmeijer/mix_proper/blob/fda1e4b19c6aabdf856b7d4948102409e0a5c9fc/lib/mix/tasks/proper.ex#L30-L35:">filtered
+out</a>. It then calls the remaining functions in the list using
+<code>:erlang.apply/3</code>, and passes their results to <code>:proper.quickcheck/1</code> to run
+the tests.</p>
+</div>
+
+<pre>$ mix proper
+....................................................................................................
+OK: Passed 100 test(s).</pre>
+<div class="paragraph">
+<p>That&#8217;s it. The proper Mix task finds property-based test files, includes them,
+and runs each of the properties in those files through PropEr. The code above
+is the basis of <a href="https://github.com/jeffkreeftmeijer/mix_proper:">mix_proper</a>,
+which can be used in your Elixir project by
+<a href="https://github.com/jeffkreeftmeijer/mix_proper_example/blob/master/mix.exs#L24:">adding
+it as a dependency</a>.</p>
+</div>
+</div>
+</div>
+
+
+</article>
+
+<hr>
+<p>
+ Any questions, feedback or suggestions? Please respond on
+ <a rel="nofollow" href="https://twitter.com/intent/tweet?url=https://jeffkreeftmeijer.com/mix-proper/">Twitter</a>
+ (or via
+ <a rel="nofollow" href="https://twitter.com/messages/compose?recipient_id=8284992">direct message</a>)
+ or send an
+ <a rel="nofollow" href="mailto:&#x6a;&#x65;&#x66;&#x66;&#x6b;&#x72;&#x65;&#x65;&#x66;&#x74;&#x6d;&#x65;&#x69;&#x6a;&#x65;&#x72;&#x40;&#x67;&#x6d;&#x61;&#x69;&#x6c;&#x2e;&#x63;&#x6f;&#x6d;&#x3f;&#x73;&#x75;&#x62;&#x6a;&#x65;&#x63;&#x74;&#x3d;&#x4f;&#x6e;&#x20;&#x201c;&#x50;&#x72;&#x6f;&#x70;&#x65;&#x72;&#x74;&#x79;&#x2d;&#x62;&#x61;&#x73;&#x65;&#x64;&#x20;&#x74;&#x65;&#x73;&#x74;&#x69;&#x6e;&#x67;&#x20;&#x69;&#x6e;&#x20;&#x45;&#x6c;&#x69;&#x78;&#x69;&#x72;&#x20;&#x75;&#x73;&#x69;&#x6e;&#x67;&#x20;&#x50;&#x72;&#x6f;&#x70;&#x45;&#x72;&#x201d;">e-mail</a>.
+ Even better, write your own article as a response.
+</p>
+<hr/>
+<nav>
+ <a href="/" rel="author">Jeff Kreeftmeijer</a>
+ <a href="/archive/">Archive</a>
+ <a rel="me" href="https://twitter.com/jkreeftmeijer">Twitter</a>
+ <a rel="me" href="https://github.com/jeffkreeftmeijer">Github</a>
+ <a href="/feed.xml">RSS</a>
+</nav>
+
+<amp-analytics type="googleanalytics">
+<script type="application/json">
+{
+ "vars": {
+ "account": "UA-12888762-1"
+ },
+ "triggers": {
+ "trackPageview": {
+ "on": "visible",
+ "request": "pageview"
+ }
+ }
+}
+</script>
+</body>
+</html>
Oops, something went wrong.

0 comments on commit 447d0ea

Please sign in to comment.