<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://servletcloud.github.io/blog/feed.xml" rel="self" type="application/atom+xml" /><link href="https://servletcloud.github.io/blog/" rel="alternate" type="text/html" /><updated>2025-05-04T09:08:16+00:00</updated><id>https://servletcloud.github.io/blog/feed.xml</id><title type="html">Vladimir Korobkov</title><subtitle>posts, thoughts and notes</subtitle><entry><title type="html">Property‑Based Testing: How a 10‑Minute Test Caught a Hidden Inconsistency</title><link href="https://servletcloud.github.io/blog/2025/05/04/pbt-1.html" rel="alternate" type="text/html" title="Property‑Based Testing: How a 10‑Minute Test Caught a Hidden Inconsistency" /><published>2025-05-04T00:00:00+00:00</published><updated>2025-05-04T00:00:00+00:00</updated><id>https://servletcloud.github.io/blog/2025/05/04/pbt-1</id><content type="html" xml:base="https://servletcloud.github.io/blog/2025/05/04/pbt-1.html"><![CDATA[<p>Property‑based testing (PBT) often looks theoretical, but it can rescue everyday projects.
I spent <strong>10 minutes</strong> on a PBT check and spotted non‑idempotent behavior in Google’s popular <a href="https://github.com/google/libphonenumber">libphonenumber</a> library.</p>

<h2 id="formatters">Formatters</h2>

<p>We rely on <em>formatters</em> — like <code class="language-plaintext highlighter-rouge">normalize_email()</code> or <code class="language-plaintext highlighter-rouge">PhoneNumberFormatter.format()</code> — to make data look the same everywhere.
In a microservices world, the same record can be saved dozens of times in one workflow, so an inconsistent formatter can quietly corrupt data and break audit trails.</p>

<p>How many times have we all seen code like this in the model layer of our frameworks? I’m showing both Ruby and Java because this pattern pops up across languages:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">before_validation</span> <span class="ss">:normalize_email</span>

<span class="k">def</span> <span class="nf">normalize_email</span>
  <span class="nb">self</span><span class="p">.</span><span class="nf">email</span> <span class="o">=</span> <span class="n">format_email</span><span class="p">(</span><span class="n">email</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>
<p>or</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@PrePersist</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">normalizePhone</span><span class="o">()</span> <span class="o">{</span>
    <span class="k">this</span><span class="o">.</span><span class="na">phone</span> <span class="o">=</span> <span class="nc">PhoneFormatter</span><span class="o">.</span><span class="na">format</span><span class="o">(</span><span class="n">phone</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Looks familiar, doesn’t it?</p>

<h2 id="the-formatter-under-test">The Formatter Under Test</h2>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">com.google.i18n.phonenumbers.NumberParseException</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">com.google.i18n.phonenumbers.PhoneNumberUtil</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">com.google.i18n.phonenumbers.Phonenumber</span><span class="o">;</span>

<span class="kd">class</span> <span class="nc">PhoneNumberFormatter</span> <span class="o">{</span>
  <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">PhoneNumberUtil</span> <span class="n">phoneUtil</span> <span class="o">=</span> <span class="nc">PhoneNumberUtil</span><span class="o">.</span><span class="na">getInstance</span><span class="o">();</span>

  <span class="kd">public</span> <span class="kd">static</span> <span class="nc">String</span> <span class="nf">format</span><span class="o">(</span><span class="nc">String</span> <span class="n">input</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">NumberParseException</span> <span class="o">{</span>
    <span class="nc">Phonenumber</span><span class="o">.</span><span class="na">PhoneNumber</span> <span class="n">number</span> <span class="o">=</span> <span class="n">phoneUtil</span><span class="o">.</span><span class="na">parse</span><span class="o">(</span><span class="n">input</span><span class="o">,</span> <span class="s">"US"</span><span class="o">);</span>
    <span class="k">return</span> <span class="n">phoneUtil</span><span class="o">.</span><span class="na">format</span><span class="o">(</span><span class="n">number</span><span class="o">,</span> <span class="nc">PhoneNumberUtil</span><span class="o">.</span><span class="na">PhoneNumberFormat</span><span class="o">.</span><span class="na">NATIONAL</span><span class="o">);</span>
  <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Simple, right? Parse a string, format it as a U.S. number, move on (see the <a href="https://github.com/google/libphonenumber">libphonenumber</a> docs for details).</p>

<h2 id="putting-pbt-to-work">Putting PBT to Work</h2>

<p>I used <a href="https://jqwik.net/">JQwik</a>—a JUnit 5‑style PBT library—to throw <strong>1,000,000 random digit strings</strong> at the formatter. The rule was simple:</p>

<blockquote>
  <p>Formatting twice should give the same result</p>
</blockquote>

<p>The test:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">net.jqwik.api.*</span><span class="o">;</span>

<span class="nd">@Property</span><span class="o">(</span><span class="n">tries</span> <span class="o">=</span> <span class="mi">1_000_000</span><span class="o">)</span> <span class="c1">// 1,000,000 trials usually finish in milliseconds</span>
<span class="kt">void</span> <span class="nf">formattingIsConsistent</span><span class="o">(</span><span class="nd">@ForAll</span><span class="o">(</span><span class="s">"phoneNumbers"</span><span class="o">)</span> <span class="nc">String</span> <span class="n">input</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">NumberParseException</span> <span class="o">{</span>
  <span class="nc">String</span> <span class="n">once</span><span class="o">;</span>
  <span class="k">try</span> <span class="o">{</span>
    <span class="n">once</span> <span class="o">=</span> <span class="nc">PhoneNumberFormatter</span><span class="o">.</span><span class="na">format</span><span class="o">(</span><span class="n">input</span><span class="o">);</span>
  <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">NumberParseException</span> <span class="n">ignored</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">return</span><span class="o">;</span> <span class="c1">// skip invalid inputs</span>
  <span class="o">}</span>

  <span class="nc">String</span> <span class="n">twice</span> <span class="o">=</span> <span class="nc">PhoneNumberFormatter</span><span class="o">.</span><span class="na">format</span><span class="o">(</span><span class="n">once</span><span class="o">);</span>
  
  <span class="n">assertThat</span><span class="o">(</span><span class="n">twice</span><span class="o">)</span>
      <span class="o">.</span><span class="na">withFailMessage</span><span class="o">(</span><span class="s">"Failed for: '%s' → '%s' → '%s'"</span><span class="o">,</span> <span class="n">input</span><span class="o">,</span> <span class="n">once</span><span class="o">,</span> <span class="n">twice</span><span class="o">)</span>
      <span class="o">.</span><span class="na">isEqualTo</span><span class="o">(</span><span class="n">once</span><span class="o">);</span>
<span class="o">}</span>

<span class="nd">@Provide</span>
<span class="nc">Arbitrary</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="nf">phoneNumbers</span><span class="o">()</span> <span class="o">{</span>
  <span class="k">return</span> <span class="nc">Arbitraries</span><span class="o">.</span><span class="na">strings</span><span class="o">().</span><span class="na">withChars</span><span class="o">(</span><span class="s">"1234567890"</span><span class="o">).</span><span class="na">ofMaxLength</span><span class="o">(</span><span class="mi">14</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="the-surprise">The Surprise</h2>

<blockquote>
  <p>Consistency failed for input: ‘0118251144’ → ‘051-144’ → ‘051144’</p>
</blockquote>

<p><em>Hypothesis</em>: the <code class="language-plaintext highlighter-rouge">formatter</code> treats national dial codes differently on re‑parse.</p>

<p>On the first pass Google’s library inserts a hyphen; on the second it drops it. That one‑character drift can ripple through logs, duplicate‑detection jobs, or billing systems—and <strong>it would be hard to spot with hand‑picked test cases.</strong></p>

<h2 id="takeaway">Takeaway</h2>

<p>Ten minutes of PBT saved what could have been <strong>days of production firefighting</strong>. If a single assertion can surface issues like this, imagine what a small suite could do for your codebase!</p>

<p>Have you tried PBT? Share your story</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Property‑based testing (PBT) often looks theoretical, but it can rescue everyday projects. I spent 10 minutes on a PBT check and spotted non‑idempotent behavior in Google’s popular libphonenumber library.]]></summary></entry></feed>