<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>jdlm.info</title>
    <description>John Lees-Miller&apos;s personal website, such as it is. Develop the lessons and pass them on.
</description>
    <link>http://jdlm.info/</link>
    <atom:link href="http://jdlm.info/feed.xml" rel="self" type="application/rss+xml"/>
    <pubDate>Mon, 13 Apr 2026 10:05:55 +0000</pubDate>
    <lastBuildDate>Mon, 13 Apr 2026 10:05:55 +0000</lastBuildDate>
    <generator>Jekyll v3.10.0</generator>
    
      <item>
        <title>Bootstrap Confidence Intervals in SQL for PostgreSQL and BigQuery</title>
        <description>&lt;p&gt;A confidence interval is a good way to express the uncertainty in an estimate. This post is about how to calculate approximate confidence intervals in portable (mostly) standard SQL using &lt;a href=&quot;https://en.wikipedia.org/wiki/Bootstrapping_(statistics)&quot;&gt;bootstrapping&lt;/a&gt;. We’ll also see that BigQuery is surprisingly fast at running the required bootstrap calculations, which makes it easy to add a confidence interval to nearly any point estimate you calculate in BigQuery.&lt;/p&gt;

&lt;p&gt;The code for this article is &lt;a href=&quot;https://github.com/jdleesmiller/sql-bootstrap&quot;&gt;open source&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id=&quot;background-confidence-intervals-and-the-bootstrap&quot;&gt;Background: Confidence Intervals and the Bootstrap&lt;/h3&gt;

&lt;p&gt;Let’s start with some background on confidence intervals and the bootstrap, illustrated with a small example. If you already know all about these, feel free to &lt;a href=&quot;#the-bootstrap-in-sql&quot;&gt;skip to the queries&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Suppose we want to find the average mass of an (adult, domestic) cat &lt;sup id=&quot;fnref:cat-mass&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:cat-mass&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;, and we’ve started by selecting 10 cats at random and measuring their masses in kilograms:&lt;/p&gt;

&lt;div class=&quot;small-table&quot;&gt;

  &lt;table&gt;
    &lt;thead&gt;
      &lt;tr&gt;
        &lt;th style=&quot;text-align: left&quot;&gt;Name&lt;/th&gt;
        &lt;th style=&quot;text-align: right&quot;&gt;Mass (kg)&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td style=&quot;text-align: left&quot;&gt;Apollo&lt;/td&gt;
        &lt;td style=&quot;text-align: right&quot;&gt;3.2&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;text-align: left&quot;&gt;Bean&lt;/td&gt;
        &lt;td style=&quot;text-align: right&quot;&gt;2.4&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;text-align: left&quot;&gt;Casper&lt;/td&gt;
        &lt;td style=&quot;text-align: right&quot;&gt;6.9&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;text-align: left&quot;&gt;Daisy&lt;/td&gt;
        &lt;td style=&quot;text-align: right&quot;&gt;3.2&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;text-align: left&quot;&gt;Ella&lt;/td&gt;
        &lt;td style=&quot;text-align: right&quot;&gt;5.1&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;text-align: left&quot;&gt;Finn&lt;/td&gt;
        &lt;td style=&quot;text-align: right&quot;&gt;3.5&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;text-align: left&quot;&gt;Ginger&lt;/td&gt;
        &lt;td style=&quot;text-align: right&quot;&gt;5.9&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;text-align: left&quot;&gt;Harley&lt;/td&gt;
        &lt;td style=&quot;text-align: right&quot;&gt;3.3&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;text-align: left&quot;&gt;Iago&lt;/td&gt;
        &lt;td style=&quot;text-align: right&quot;&gt;5.5&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;text-align: left&quot;&gt;Jasper&lt;/td&gt;
        &lt;td style=&quot;text-align: right&quot;&gt;5.4&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;Mean&lt;/strong&gt;&lt;/td&gt;
        &lt;td style=&quot;text-align: right&quot;&gt;4.4&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;Std Dev&lt;/strong&gt;&lt;/td&gt;
        &lt;td style=&quot;text-align: right&quot;&gt;1.5&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;

&lt;/div&gt;

&lt;p&gt;The &lt;em&gt;sample mean&lt;/em&gt; for these ten cats is 4.4kg, with a standard deviation of 1.5kg. What can we now say about the &lt;em&gt;population mean&lt;/em&gt; for all cats? The sample mean is our best estimate for the population mean, but it may be inaccurate, because it is based on a (very) small subset of the population; this inaccuracy is called sampling error. A confidence interval quantifies sampling error by calculating an interval that we can be confident, up to a defined confidence level, contains the population mean. For example, one of the methods we’ll use below calculates a confidence interval at the 95% level for the average mass of a cat as [3.4kg, 5.5kg] based on these 10 measurements.&lt;/p&gt;

&lt;p&gt;Before looking at how to calculate that interval, it’s worth spending a few words on what it means. Firstly, it is not a claim that 95% of all cats weigh between 3.4kg and 5.5kg; instead, it is a claim that the &lt;em&gt;mean&lt;/em&gt; over all cats is likely in this range. Secondly, it is not strictly speaking a claim that the population mean lies in that range “with probability 0.95” &lt;sup id=&quot;fnref:credible-interval&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:credible-interval&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;. Instead, the preferred interpretation is that, if one were to repeat the whole experiment many times, each time re-running all the data collection and analysis on a new sample to calculate a 95% confidence interval, the (fixed) population mean would lie outside the calculated interval in only 5% of the repeats. This is somewhat sobering, firstly because we have no way of knowing if the one experiment we actually did was one of the unlucky 5%, and secondly because, even if we do everything right, if we do a lot of different experiments we should expect to be wrong in 5% of them. That is still, however, better than one is likely to do with point estimates alone!&lt;/p&gt;

&lt;p&gt;On to the confidence interval calculations without bootstrapping. Under the (here reasonable) assumption that the population distribution is normal, which is to say that the distribution of the mass of all cats is normal, a confidence interval can be obtained from the quantiles of the &lt;a href=&quot;https://en.wikipedia.org/wiki/Student%27s_t-distribution&quot;&gt;\(t\)-distribution&lt;/a&gt;. In particular, for a sample of size \(n\) with sample mean \(\bar{x}\) and (sample) standard deviation \(s\), the endpoints of a \(C \times 100 \%\) confidence interval are given by
\begin{equation}
\bar{x} \pm Q_t\left(\frac{1-C}{2}, n - 1\right) \frac{s}{\sqrt{n}}
\label{t-ci}
\end{equation}
where \(Q_t(\alpha, k)\) denotes the quantile function of the \(t\)-distribution with \(k\) degrees of freedom, evaluated at quantile \(\alpha\), where in this case \(\alpha = \frac{1-C}{2}\) and \(k = n - 1\). The second term in the above equation is the standard error of the sample mean, scaled by the \(Q_t\) quantile. For a 95% confidence level and sample size 10, \(\alpha = 0.025\), \(k = 9\), and the \(Q_t\) factor is -2.26, which leads to the interval [3.4kg, 5.5kg], as noted above &lt;sup id=&quot;fnref:q196&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:q196&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;

&lt;p&gt;The process for finding the bootstrap confidence interval looks very different. Rather than a formula, it is an algorithm. To calculate a bootstrap confidence interval, we repeatedly &lt;em&gt;resample&lt;/em&gt; this original sample, with replacement, and compute the mean for each resample. The first such resampling might be:&lt;/p&gt;

&lt;div class=&quot;small-table&quot;&gt;

  &lt;table&gt;
    &lt;thead&gt;
      &lt;tr&gt;
        &lt;th style=&quot;text-align: left&quot;&gt;Name&lt;/th&gt;
        &lt;th style=&quot;text-align: right&quot;&gt;Mass (kg)&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td style=&quot;text-align: left&quot;&gt;Apollo&lt;/td&gt;
        &lt;td style=&quot;text-align: right&quot;&gt;3.2&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;text-align: left&quot;&gt;Bean&lt;/td&gt;
        &lt;td style=&quot;text-align: right&quot;&gt;2.4&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;text-align: left&quot;&gt;Casper&lt;/td&gt;
        &lt;td style=&quot;text-align: right&quot;&gt;6.9&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;text-align: left&quot;&gt;Casper&lt;/td&gt;
        &lt;td style=&quot;text-align: right&quot;&gt;6.9&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;text-align: left&quot;&gt;Casper&lt;/td&gt;
        &lt;td style=&quot;text-align: right&quot;&gt;6.9&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;text-align: left&quot;&gt;Casper&lt;/td&gt;
        &lt;td style=&quot;text-align: right&quot;&gt;6.9&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;text-align: left&quot;&gt;Ella&lt;/td&gt;
        &lt;td style=&quot;text-align: right&quot;&gt;5.1&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;text-align: left&quot;&gt;Ella&lt;/td&gt;
        &lt;td style=&quot;text-align: right&quot;&gt;5.1&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;text-align: left&quot;&gt;Finn&lt;/td&gt;
        &lt;td style=&quot;text-align: right&quot;&gt;3.5&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;text-align: left&quot;&gt;Finn&lt;/td&gt;
        &lt;td style=&quot;text-align: right&quot;&gt;3.5&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;Mean&lt;/strong&gt;&lt;/td&gt;
        &lt;td style=&quot;text-align: right&quot;&gt;5.1&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;Std Dev&lt;/strong&gt;&lt;/td&gt;
        &lt;td style=&quot;text-align: right&quot;&gt;1.8&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;

&lt;/div&gt;

&lt;p&gt;Here we’ve chosen Casper four times and left out several of the other cats altogether, as can happen when resampling with replacement. Casper is a rather heavy cat, which has pulled up the mean for this resample to 5.1kg. The second resample might then be:&lt;/p&gt;

&lt;div class=&quot;small-table&quot;&gt;

  &lt;table&gt;
    &lt;thead&gt;
      &lt;tr&gt;
        &lt;th style=&quot;text-align: left&quot;&gt;Name&lt;/th&gt;
        &lt;th style=&quot;text-align: right&quot;&gt;Mass (kg)&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td style=&quot;text-align: left&quot;&gt;Casper&lt;/td&gt;
        &lt;td style=&quot;text-align: right&quot;&gt;6.9&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;text-align: left&quot;&gt;Casper&lt;/td&gt;
        &lt;td style=&quot;text-align: right&quot;&gt;6.9&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;text-align: left&quot;&gt;Daisy&lt;/td&gt;
        &lt;td style=&quot;text-align: right&quot;&gt;3.2&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;text-align: left&quot;&gt;Finn&lt;/td&gt;
        &lt;td style=&quot;text-align: right&quot;&gt;3.5&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;text-align: left&quot;&gt;Finn&lt;/td&gt;
        &lt;td style=&quot;text-align: right&quot;&gt;3.5&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;text-align: left&quot;&gt;Finn&lt;/td&gt;
        &lt;td style=&quot;text-align: right&quot;&gt;3.5&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;text-align: left&quot;&gt;Ginger&lt;/td&gt;
        &lt;td style=&quot;text-align: right&quot;&gt;5.9&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;text-align: left&quot;&gt;Harley&lt;/td&gt;
        &lt;td style=&quot;text-align: right&quot;&gt;3.3&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;text-align: left&quot;&gt;Harley&lt;/td&gt;
        &lt;td style=&quot;text-align: right&quot;&gt;3.3&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;text-align: left&quot;&gt;Jasper&lt;/td&gt;
        &lt;td style=&quot;text-align: right&quot;&gt;5.4&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;Mean&lt;/strong&gt;&lt;/td&gt;
        &lt;td style=&quot;text-align: right&quot;&gt;4.6&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;Std Dev&lt;/strong&gt;&lt;/td&gt;
        &lt;td style=&quot;text-align: right&quot;&gt;1.6&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;

&lt;/div&gt;

&lt;p&gt;This time, some of the cats that were missing from the first sample reappear, and there are fewer Caspers pulling up the mean, so it returns to 4.6kg, closer to our original sample. If we then repeat this process 998 more times to collect a total of 1000 resampled means, we are likely to find something like the following bootstrap distribution:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
&lt;img src=&quot;/assets/sql-bootstrap/cats-example.svg&quot; alt=&quot;Histogram of the empirical bootstrap distribution of the mean mass of a cat. The distribution is roughly bell-shaped, centered roughly on the sample mean of 4.4kg, with tails from roughly 3kg to roughly 6kg.&quot; style=&quot;max-height: 40em;&quot; /&gt;
&lt;/p&gt;

&lt;p&gt;To form the desired 95% confidence interval, the simplest approach is to simply read off the the 2.5% and 97.5% quantiles from this empirical bootstrap distribution as the 95% confidence interval, which in this case gives the interval [3.6kg, 5.3kg].&lt;/p&gt;

&lt;p&gt;One way to think about bootstrapping is as a ‘what if’ sensitivity analysis with the observations in a sample. It essentially asks for each observation, what would our estimate look like if we had not collected this observation? Or if we’d seen this observation several times instead of just once? If the sample is large, or if the observations are fairly similar, then losing or double counting a few of them shouldn’t make a big difference, and bootstrap distribution will be sharp and the confidence interval tight. If, on the other hand, the sample is smaller, or there is more variability in the observations, then the bootstrap distribution will be more dispersed and the confidence interval wider.&lt;/p&gt;

&lt;p&gt;This example hints at several important caveats:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Bootstrap CIs require a lot of computation. There is no hard and fast rule for the number of resamples that one should use, but 1000 is generally regarded as the minimum for calculation of confidence intervals. This means that instead of computing a statistic once, bootstrapping requires that it be computed thousands of times. Fortunately, the required computation can be efficiently parallelized, as we shall see below.&lt;/li&gt;
  &lt;li&gt;Bootstrap CIs are approximate. In &lt;a href=&quot;https://en.wikipedia.org/wiki/Confidence_interval#Confidence_interval_for_specific_distributions&quot;&gt;many common cases&lt;/a&gt;, there are more accurate and efficient methods of calculating confidence intervals. They should be used where possible. Bootstrapping is still a useful technique for more complicated cases or as an additional check on other methods.&lt;/li&gt;
  &lt;li&gt;The ‘percentile bootstrap’ approach of calculating the confidence interval directly from the quantiles of the bootstrap distribution is simple to implement and intuitively appealing, but it is known to produce intervals that are too narrow, particularly for small sample sizes. This may explain why the bootstrap interval obtained above was narrower than the \(t\) interval. There are some ways to correct for this &lt;sup id=&quot;fnref:correction&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:correction&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;4&lt;/a&gt;&lt;/sup&gt;, but this approach will do for now.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;the-bootstrap-in-sql&quot;&gt;The Bootstrap in SQL&lt;/h3&gt;

&lt;p&gt;Now that we’ve seen the idea behind the bootstrap, let’s see an example of how to implement it in SQL. Suppose we have scaled up our cat weighing experiment and now have a table, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cats&lt;/code&gt;, with 10000 rows. Each row records a unique identifier for the cat and its mass in kilograms.&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cats&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;count&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;-------&lt;/span&gt;
&lt;span class=&quot;mi&quot;&gt;10000&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;row&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cats&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;random&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;LIMIT&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;  &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;       &lt;span class=&quot;n&quot;&gt;mass&lt;/span&gt;       
&lt;span class=&quot;c1&quot;&gt;------+------------------&lt;/span&gt;
 &lt;span class=&quot;mi&quot;&gt;3091&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;25136586892616&lt;/span&gt;
 &lt;span class=&quot;mi&quot;&gt;5680&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;34285504738124&lt;/span&gt;
 &lt;span class=&quot;mi&quot;&gt;5895&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;63979916384868&lt;/span&gt;
 &lt;span class=&quot;mi&quot;&gt;1952&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;  &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1710561140116&lt;/span&gt;
 &lt;span class=&quot;mi&quot;&gt;3847&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;82984705465995&lt;/span&gt;
 &lt;span class=&quot;mi&quot;&gt;2861&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;33598448266297&lt;/span&gt;
  &lt;span class=&quot;mi&quot;&gt;592&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;52217568717482&lt;/span&gt;
 &lt;span class=&quot;mi&quot;&gt;2915&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;87406259543517&lt;/span&gt;
 &lt;span class=&quot;mi&quot;&gt;6338&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;  &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0396866933194&lt;/span&gt;
 &lt;span class=&quot;mi&quot;&gt;6726&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;71685250612103&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;rows&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The mean mass for this sample is given by:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;avg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mass&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cats&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;avg&lt;/span&gt;        
&lt;span class=&quot;c1&quot;&gt;-------------------&lt;/span&gt;
 &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;492052081409403&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;row&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The following query calculates a 95% CI around this estimate using 1000 resamples (for PostgreSQL; the query for BigQuery is &lt;a href=&quot;https://github.com/jdleesmiller/sql-bootstrap/blob/d654236aa6f669fcd5ab68c3827d40eeb95d3092/example-sql/bq-bootstrap-pure-percent.sql&quot;&gt;here&lt;/a&gt;):&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;WITH&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bootstrap_indexes&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;generate_series&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bootstrap_index&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;bootstrap_data&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mass&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ROW_NUMBER&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;OVER&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data_index&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cats&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;bootstrap_map&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;floor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;random&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data_index&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bootstrap_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data_index&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;bootstrap_index&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bootstrap_data&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;JOIN&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bootstrap_indexes&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ON&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;TRUE&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;bootstrap&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bootstrap_index&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;avg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mass&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mass_avg&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bootstrap_map&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;JOIN&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bootstrap_data&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;USING&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data_index&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;GROUP&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bootstrap_index&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;bootstrap_ci&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;percentile_cont&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;025&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;WITHIN&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;GROUP&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mass_avg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mass_lo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;percentile_cont&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;975&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;WITHIN&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;GROUP&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mass_avg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mass_hi&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bootstrap&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;sample&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;avg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mass&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mass_avg&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cats&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sample&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;JOIN&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bootstrap_ci&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ON&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;TRUE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Let’s take it one &lt;a href=&quot;https://www.postgresql.org/docs/13/queries-with.html&quot;&gt;CTE&lt;/a&gt; at a time:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bootstrap_indexes&lt;/code&gt; enumerates the bootstrap resamples, 1 to 1000, as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bootstrap_index&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bootstrap_data&lt;/code&gt; generates a contiguous sequence of row numbers, one for each row in the input data, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data_index&lt;/code&gt;. (Here I’ve used &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id&lt;/code&gt; as the natural way to order the rows, but you could use anything.) The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;- 1&lt;/code&gt; is important, because it makes the sequence start from 0 rather than 1, and the next CTE will generate random indexes starting at 0.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bootstrap_map&lt;/code&gt; performs the resampling with replacement by generating 10000 random integers in the range of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data_index&lt;/code&gt; for each of the 1000 resamples. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;JOIN bootstrap_indexes ON TRUE&lt;/code&gt; produces the full &lt;a href=&quot;https://en.wikipedia.org/wiki/Cartesian_product&quot;&gt;Cartesian product&lt;/a&gt; of the bootstrap and data indexes, so for 1000 resamples of 10000 observations, there are 10 million rows in this CTE.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bootstrap&lt;/code&gt; computes the mean mass for each resample by joining the bootstrap data with the bootstrap map by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data_index&lt;/code&gt;, grouping by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bootstrap_index&lt;/code&gt;, and calculating the mean within each group (i.e. within each resample).&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bootstrap_ci&lt;/code&gt; uses the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;percentile_cont&lt;/code&gt; &lt;a href=&quot;https://www.postgresql.org/docs/13/functions-aggregate.html&quot;&gt;ordered-set aggregate function&lt;/a&gt; to find the 2.5% and 97.5% percentiles of the empirical bootstrap distribution.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sample&lt;/code&gt; computes the most likely estimate as we did above.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Finally, we put them all together to get a single row with the most likely estimate, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mass_avg&lt;/code&gt;, as above, and the confidence interval bounds, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mass_lo&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mass_hi&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;     &lt;span class=&quot;n&quot;&gt;mass_avg&lt;/span&gt;      &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;      &lt;span class=&quot;n&quot;&gt;mass_lo&lt;/span&gt;      &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;mass_hi&lt;/span&gt;      
&lt;span class=&quot;c1&quot;&gt;-------------------+-------------------+------------------&lt;/span&gt;
 &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;492052081409403&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;470893493719024&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;51113963545513&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;row&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That is, our estimate here is 4.49kg with 95% CI [4.47kg, 4.51kg]. In this case, the data were &lt;a href=&quot;https://github.com/jdleesmiller/sql-bootstrap/blob/d654236aa6f669fcd5ab68c3827d40eeb95d3092/make-example-data.R&quot;&gt;generated&lt;/a&gt; with a true mass of 4.5kg, so the mean is not far out, and the true rate is within the 95% confidence interval, as we’d expect to happen 95% of the time. (If you rerun the same query on the example data, you may get somewhat different numbers due to randomness in the bootstrap sampling, but with 1000 resamples they should not be very different very often.)&lt;/p&gt;

&lt;p&gt;This query takes ~20s to run on my instance, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EXPLAIN ANALYZE&lt;/code&gt; shows most of that time is spent joining the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bootstrap_map&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bootstrap_data&lt;/code&gt; back together in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bootstrap&lt;/code&gt; CTE. Let’s see if we can speed it up.&lt;/p&gt;

&lt;h3 id=&quot;the-poisson-bootstrap-in-sql&quot;&gt;The Poisson Bootstrap in SQL&lt;/h3&gt;

&lt;p&gt;The overall effect of resampling with replacement is that each of the observations in the original sample is used a random number of times in any given resample. One way to think of this random number of times is as a &lt;em&gt;bootstrap weight&lt;/em&gt; for each observation in each resample. Returning to the cats example above, the first two resamples could instead have been expressed in terms of bootstrap weights, as follows:&lt;/p&gt;

&lt;table style=&quot;margin-bottom: 30px;&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th colspan=&quot;2&quot;&gt;Original Sample&lt;/th&gt;
      &lt;th&gt;Resample 1&lt;/th&gt;
      &lt;th&gt;Resample 2&lt;/th&gt;
      &lt;th&gt;&amp;hellip;&lt;/th&gt;
      &lt;th&gt;Resample 1000&lt;/th&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;Name&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;Mass (kg)&lt;/th&gt;
      &lt;th&gt;Bootstrap Weight&lt;/th&gt;
      &lt;th&gt;Bootstrap Weight&lt;/th&gt;
      &lt;th&gt;&amp;hellip;&lt;/th&gt;
      &lt;th&gt;Bootstrap Weight&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;Apollo&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;3.2&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;1&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;&amp;hellip;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;Bean&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;2.4&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;1&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;&amp;hellip;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;Casper&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;6.9&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;4&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;2&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;&amp;hellip;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;Daisy&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;3.2&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;1&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;&amp;hellip;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;Ella&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;5.1&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;2&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;&amp;hellip;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;Finn&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;3.5&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;2&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;3&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;&amp;hellip;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;Ginger&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;5.9&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;1&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;&amp;hellip;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;Harley&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;3.3&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;2&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;&amp;hellip;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;Iago&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;5.5&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;&amp;hellip;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;Jasper&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;5.4&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;1&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;&amp;hellip;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;For example, Casper appears in the first resample 4 times and so has weight 4. For each resample, the weights sum up to the number of observations in the original sample, namely 10. To calculate the mean mass for each resample, we take the weighted average of the cat masses using the bootstrap weights.&lt;/p&gt;

&lt;p&gt;In general, for a sample of \(n\) original observations, the bootstrap weights for each resample jointly follow a \(\textrm{Multinomial}(n,\frac{1}{n},\ldots,\frac{1}{n})\) &lt;a href=&quot;https://en.wikipedia.org/wiki/Multinomial_distribution&quot;&gt;distribution&lt;/a&gt;. The approach in the &lt;a href=&quot;https://www.unofficialgoogledatascience.com/2015/08/an-introduction-to-poisson-bootstrap26.html&quot;&gt;Poisson bootstrap&lt;/a&gt; is to make two simplifying approximations to how we generate these bootstrap weights:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;Approximate the \(n\)–dimensional multinomial distribution with \(n\) independent \(\textrm{Binomial}(n, \frac{1}{n})\) &lt;a href=&quot;https://en.wikipedia.org/wiki/Binomial_distribution&quot;&gt;distributions&lt;/a&gt;. The advantage is that the independent binomial distributions for each observation can be sampled in parallel &lt;sup id=&quot;fnref:multinomial-binomial&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:multinomial-binomial&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;5&lt;/a&gt;&lt;/sup&gt;. The disadvantage is that the total number of observations in a resample, which was constrained to be exactly \(n\) in the multinomial case, may not add up to \(n\) in the binomial case. When computing a statistic like a mean, where we divide through by the number of observations, this turns out not to make much difference, provided \(n\) is large enough to avoid very sparse resamples (\(n \gtrapprox 100\)).&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Approximate the \(\textrm{Binomial}(n, \frac{1}{n})\) distribution by a \(\textrm{Poisson}(1)\) &lt;a href=&quot;https://en.wikipedia.org/wiki/Poisson_distribution&quot;&gt;distribution&lt;/a&gt;. This is a &lt;a href=&quot;https://en.wikipedia.org/wiki/Binomial_distribution#Poisson_approximation&quot;&gt;good approximation&lt;/a&gt; for any reasonably large \(n\), and it avoids having the weights depend on \(n\), which is again helpful for parallel running.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These simplifications allow us to avoid the join that was the most expensive part of the ‘pure’ bootstrap; instead, the query attaches the Poisson weights directly to the observations and computes the required weighted mean. The Poisson bootstrap query for the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cats&lt;/code&gt; example with 1000 resamples looks like this (for PostgreSQL; the query for BigQuery is &lt;a href=&quot;https://github.com/jdleesmiller/sql-bootstrap/blob/d654236aa6f669fcd5ab68c3827d40eeb95d3092/example-sql/bq-bootstrap-poisson-percent.sql&quot;&gt;here&lt;/a&gt;):&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;WITH&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bootstrap_indexes&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;generate_series&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bootstrap_index&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;bootstrap_data&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mass&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bootstrap_index&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;random&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bootstrap_u&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cats&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;JOIN&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bootstrap_indexes&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ON&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;TRUE&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;bootstrap_weights&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bootstrap_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;CASE&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bootstrap_u&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;367879441171442&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bootstrap_u&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;735758882342885&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bootstrap_u&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;919698602928606&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bootstrap_u&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;981011843123846&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bootstrap_u&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;996340153172656&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bootstrap_u&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;999405815182418&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bootstrap_u&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;999916758850712&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;6&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bootstrap_u&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;999989750803325&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;7&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bootstrap_u&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;999998874797402&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bootstrap_u&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;999999888574522&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;9&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bootstrap_u&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;999999989952234&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bootstrap_u&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;999999999168389&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;11&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bootstrap_u&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;999999999936402&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;12&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bootstrap_u&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;99999999999548&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;13&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bootstrap_u&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;9999999999997&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;14&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;ELSE&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;15&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;END&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bootstrap_weight&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bootstrap_data&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;bootstrap&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bootstrap_index&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;sum&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bootstrap_weight&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mass&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;sum&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bootstrap_weight&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mass_avg&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bootstrap_weights&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;GROUP&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bootstrap_index&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;bootstrap_ci&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;percentile_cont&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;025&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;WITHIN&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;GROUP&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mass_avg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mass_lo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;percentile_cont&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;975&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;WITHIN&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;GROUP&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mass_avg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mass_hi&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bootstrap&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;sample&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;avg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mass&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mass_avg&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cats&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sample&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;JOIN&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bootstrap_ci&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ON&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;TRUE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Let’s again take it one CTE at a time:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bootstrap_indexes&lt;/code&gt; is as it was in the pure case.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bootstrap_data&lt;/code&gt; generates 1000 \(\textrm{Uniform}(0,1)\) random numbers for each of the 10000 observations; like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bootstrap_map&lt;/code&gt; in the ‘pure’ bootstrap query above, it has 10 million rows.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bootstrap_weights&lt;/code&gt; converts these variates from the uniform distribution to the Poisson distribution using &lt;a href=&quot;https://en.wikipedia.org/wiki/Inverse_transform_sampling&quot;&gt;inverse transform sampling&lt;/a&gt;, in which we invert the Poisson cumulative distribution function &lt;sup id=&quot;fnref:do-not-combine&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:do-not-combine&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;6&lt;/a&gt;&lt;/sup&gt;. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CASE&lt;/code&gt; statement here is basically an unrolled loop generated from &lt;a href=&quot;https://github.com/jdleesmiller/sql-bootstrap/blob/d654236aa6f669fcd5ab68c3827d40eeb95d3092/make-sql-bootstrap.R#L22-L35&quot;&gt;this R code&lt;/a&gt;; it encodes that, when drawing from the \(\textrm{Poisson}(1)\) distribution, one obtains 0 with probability 0.368, 0 or 1 with probability 0.736, 0, 1, or 2 with probability 0.920, and so on, up to 15 where the probability is so close to 1 that we start to hit the limits of 64-bit floating point numbers.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bootstrap&lt;/code&gt; again computes the mean mass for each resample, but this time it does so by finding a weighted average using the bootstrap weights.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bootstrap_ci&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sample&lt;/code&gt; are exactly as before.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This query produces essentially the same results, but in ~12s rather than ~20s for the ‘pure’ bootstrap query above, which may not seem like much given all the extra mathematics, but it can be a larger savings for larger datasets.&lt;/p&gt;

&lt;h3 id=&quot;benchmark-results&quot;&gt;Benchmark Results&lt;/h3&gt;

&lt;p&gt;So, let’s see some timings for the pure and Poisson approaches and PostgreSQL and BigQuery, as we vary the size of the sample:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
&lt;img src=&quot;/assets/sql-bootstrap/benchmark.svg&quot; alt=&quot;Running times with Postgres for the Pure Bootstrap and Poisson Bootstrap increase from near zero for one thousand cats to roughly 2200s and 700s, respectively. Running times with BigQuery remain near zero over the entire range.&quot; style=&quot;max-height: 40em;&quot; /&gt;
&lt;/p&gt;

&lt;p&gt;These are wall clock times for 1000 bootstrap resamples. The Postgres instance used here was a Google Cloud SQL instance with 4 vCPUs and 16GiB of RAM, running Postgres 15.2. Each point is based on 10 trials. The error bars are (of course!) bootstrap 95% CIs.&lt;/p&gt;

&lt;p&gt;The main conclusions are that the Poisson queries run faster than the pure queries, and that BigQuery is a lot faster than Postgres on both kinds of queries. Execution times with BigQuery remained essentially constant over the whole range. This is basically because BigQuery parallelized the bootstrap across many nodes, whereas Postgres ran it serially. The CPU times that BigQuery reported for queries that I ran manually were comparable to the wall clock times for Postgres.&lt;/p&gt;

&lt;p&gt;I had hoped Postgres would also parallelize the queries, but it did not. It only ever used one out of its four available cores. The &lt;a href=&quot;https://www.postgresql.org/docs/15/parallel-safety.html&quot;&gt;docs&lt;/a&gt; indicate that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;random&lt;/code&gt; is currently labelled as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PARALLEL RESTRICTED&lt;/code&gt;, which might be a contributing factor.&lt;/p&gt;

&lt;p&gt;It would be unwise to draw any conclusions about the absolute or relative costs of Postgres and BigQuery from these results, but I did learn a few things related to costs, so here they are. I spent £35 on running the Cloud SQL instance for a few days and £12 on 100 “flat rate flex slots” for BigQuery for a few hours. That said, the Cloud SQL instance was not always busy, and I had to rerun some tests after all my results perished in a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;make&lt;/code&gt; accident &lt;sup id=&quot;fnref:precious&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:precious&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;7&lt;/a&gt;&lt;/sup&gt;. Had I avoided that, it probably would have finished in about half the time (and cost). The queries with less than \(10^6\) cats all ran fine in BigQuery’s on-demand pricing model and apparently fit within the free tier. For the larger datasets, I hit a limit (understandably) on the amount of CPU time they were using for the bootstrap resamples, which was very large compared to the size of the input data that the query was billed on. To get around that, I had to reserve some capacity, which required putting in a quota increase request but was otherwise a fairly painless process.&lt;/p&gt;

&lt;p&gt;Finally, we should check that the generated intervals are correct. Here is a comparison with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;boot&lt;/code&gt; package from R:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
&lt;img src=&quot;/assets/sql-bootstrap/check.svg&quot; alt=&quot;There is a violin plot for each method used, and all of the violins are roughly similar in their position and shape. The line for Student&apos;s t intervals runs somewhere through the thickest part of the violin in each case, but generally there is more mass on the inside of the interval, indicating some undercoverage.&quot; style=&quot;max-height: 40em;&quot; /&gt;
&lt;/p&gt;

&lt;p&gt;Each row shows the distribution of the 95% confidence interval endpoints over 100 bootstrapping trials for a fixed sample of 100 cats, as computed with R as the baseline, and Postgres and BigQuery using the ‘pure’ and Poisson bootstrap queries above. The plots also include the \(t\) intervals from equation \eqref{t-ci} and, because this is synthetic data, the normal CI calculated from the true population variance used to generate the dataset.&lt;/p&gt;

&lt;p&gt;There is good agreement between the distributions for R and the two SQL queries, which indicates that the queries are computing the right things. All of the bootstrap percentile intervals undercover somewhat with respect to the \(t\) interval, which as noted above is a common problem for percentile intervals. There are some methods that attempt to correct for this &lt;sup id=&quot;fnref:correction:1&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:correction&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;4&lt;/a&gt;&lt;/sup&gt;. For this particular sample, the \(t\) interval is also too narrow compared to the interval obtained using the true population variance, but on average it would cover correctly here.&lt;/p&gt;

&lt;h3 id=&quot;conclusions&quot;&gt;Conclusions&lt;/h3&gt;

&lt;p&gt;We have seen how to implement bootstrap confidence intervals in (mostly) standard SQL. The full set of example queries is &lt;a href=&quot;https://github.com/jdleesmiller/sql-bootstrap/tree/d654236aa6f669fcd5ab68c3827d40eeb95d3092/example-sql&quot;&gt;here&lt;/a&gt;. The queries run remarkably quickly in BigQuery, and they are usable for relatively small samples, at least, in Postgres. Using the Poisson approximation can significantly speed up the queries.&lt;/p&gt;

&lt;p&gt;Where a &lt;a href=&quot;https://en.wikipedia.org/wiki/Confidence_interval#Confidence_interval_for_specific_distributions&quot;&gt;standard formula&lt;/a&gt; exists for confidence intervals, it is usually best to use it. However, if you do need to bootstrap, and all you have is SQL, it turns out you can do it.&lt;/p&gt;

&lt;h3 id=&quot;footnotes&quot;&gt;Footnotes&lt;/h3&gt;

&lt;script type=&quot;text/x-mathjax-config&quot;&gt;
MathJax.Hub.Config({
  TeX: { equationNumbers: { autoNumber: &quot;AMS&quot; } }
});
&lt;/script&gt;

&lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML&quot; type=&quot;text/javascript&quot;&gt;&lt;/script&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:cat-mass&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;This seems like the sort of thing that should be known, but estimates vary. Googling &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cat&lt;/code&gt; produces an info box with a range of 3.6kg–4.5kg, apparently without a definition (what quantiles?) or source. Wikipedia says &lt;a href=&quot;https://en.wikipedia.org/wiki/Cat#Size&quot;&gt;4kg-5kg&lt;/a&gt;. National Geographic gives a rather broader range of &lt;a href=&quot;https://www.nationalgeographic.com/animals/mammals/facts/domestic-cat&quot;&gt;5lb-20lb&lt;/a&gt;, which is 2.3kg–9.1kg. Anyway, it is just an example. &lt;a href=&quot;#fnref:cat-mass&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:credible-interval&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;For that, one instead wants a Bayesian interval estimate, often called a &lt;a href=&quot;https://en.wikipedia.org/wiki/Credible_interval&quot;&gt;credible interval&lt;/a&gt;. Fortunately, the two kinds of intervals do agree in many important cases. &lt;a href=&quot;#fnref:credible-interval&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:q196&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;In this formula, the exact \(Q_t\) quantile for a 95% confidence interval is often replaced with the constant 1.96, which is the corresponding quantile for the normal distribution. This approximation is good for large samples, since the \(t\) distribution approaches the Normal distribution as the sample size increases. On a sample of size 10, it yields something more like a 92% confidence interval. Put differently, if we lazily use 1.96 instead of the correct \(t\) quantile, we should expect to be wrong 8% of the time instead of 5% of the time, which is about 60% more often! Most spreadsheets now have a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;T.DIST&lt;/code&gt; function, so using the right number is not much more work. Maybe databases will catch up eventually. &lt;a href=&quot;#fnref:q196&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:correction&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;There are &lt;a href=&quot;https://en.wikipedia.org/wiki/Bootstrapping_(statistics)#Deriving_confidence_intervals_from_the_bootstrap_distribution&quot;&gt;several ways&lt;/a&gt; to calculate a confidence interval from the bootstrap distribution, of which the percentile bootstrap is the simplest. Queries for the “Studentized” bootstrap are available in the companion repo &lt;a href=&quot;https://github.com/jdleesmiller/sql-bootstrap/blob/d654236aa6f669fcd5ab68c3827d40eeb95d3092/example-sql/pg-bootstrap-pure-student.sql&quot;&gt;here for Postgres&lt;/a&gt; and &lt;a href=&quot;https://github.com/jdleesmiller/sql-bootstrap/blob/d654236aa6f669fcd5ab68c3827d40eeb95d3092/example-sql/bq-bootstrap-pure-student.sql&quot;&gt;here for BigQuery&lt;/a&gt;. The Studentized intervals seem to have better coverage, but in my experience they are quite sensitive to outliers. I am not sure there is any broad consensus on which one of these approaches is best overall. &lt;a href=&quot;#fnref:correction&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt; &lt;a href=&quot;#fnref:correction:1&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:multinomial-binomial&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;It may be helpful to think of this in physical terms. The physical analogy for the multinomial distribution would be rolling an \(n\)-sided die \(n\) times; the weight of an observation would be the number of times the die lands on the corresponding face. For the binomial approximation, it would be flipping a (very) unfair coin with probability \(\frac{1}{n}\) of coming up heads; each observation gets its own coin, which is flipped \(n\) times, and the observation’s weight is the number of heads. Instead of rolling one giant die, which might look more like a disco ball, for the whole sample, the binomial approximation lets us flip the \(n\) independent coins in parallel. &lt;a href=&quot;#fnref:multinomial-binomial&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:do-not-combine&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;It might be tempting to combine the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bootstrap_data&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bootstrap_weights&lt;/code&gt; CTEs, but &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CASE WHEN random() &amp;lt; x THEN y WHEN random() &amp;lt; z THEN w ...&lt;/code&gt; will generate a new random number for each &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WHEN&lt;/code&gt;, rather than repeatedly testing the same random number against the breaks of the target CDF. That might be an interesting way to sample from a Geometric distribution, but it is not what we want here. &lt;a href=&quot;#fnref:do-not-combine&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:precious&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;If you hit Ctrl-C to interrupt &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;make&lt;/code&gt;, it deletes the target by default, because it doesn’t want to leave half-built files hanging around. That is not what you want when the target is a CSV with all your results in it. The solution is to mark the target as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.PRECIOUS&lt;/code&gt;. &lt;a href=&quot;#fnref:precious&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</description>
        <pubDate>Sun, 11 Jun 2023 12:00:00 +0000</pubDate>
        <link>http://jdlm.info/articles/2023/06/11/bootstrap-confidence-intervals-sql-postgresql-bigquery.html</link>
        <guid isPermaLink="true">http://jdlm.info/articles/2023/06/11/bootstrap-confidence-intervals-sql-postgresql-bigquery.html</guid>
        
        
        <category>articles</category>
        
      </item>
    
      <item>
        <title>Testing with Node and Docker Compose, Part 3: End-to-End</title>
        <description>&lt;p&gt;This post is the third in a short series about automated testing in node.js web applications with Docker. So far, we have looked at &lt;a href=&quot;/articles/2019/10/19/testing-node-docker-compose-backend.html&quot;&gt;backend testing&lt;/a&gt; and &lt;a href=&quot;/articles/2020/01/12/testing-node-docker-compose-frontend.html&quot;&gt;frontend testing&lt;/a&gt;. This post will be about end-to-end testing, which covers both frontend and backend together.&lt;/p&gt;

&lt;p&gt;To illustrate, we’ll continue building and testing our example application: a simple TODO list manager, which comprises a RESTful backend and a React frontend:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;/assets/todo-demo/todo-demo-frontend.gif&quot;&gt;&lt;img src=&quot;/assets/todo-demo/todo-demo-frontend.gif&quot; alt=&quot;Create some todos and complete them&quot; style=&quot;max-width: 448px;&quot; /&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;In particular, we’ll:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Use Docker Compose &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;projects&lt;/code&gt; to create separate development and test environments, while sharing some services between the two for efficiency.&lt;/li&gt;
  &lt;li&gt;Upgrade our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin&lt;/code&gt; scripts to manage these separate ‘projects’.&lt;/li&gt;
  &lt;li&gt;Extract the storage layer from the backend so that the end-to-end tests can use it.&lt;/li&gt;
  &lt;li&gt;Set up end-to-end tests with &lt;a href=&quot;https://pptr.dev/&quot;&gt;puppeteer&lt;/a&gt; in Docker.&lt;/li&gt;
  &lt;li&gt;Promote some frontend integration tests using &lt;a href=&quot;https://github.com/jsdom/jsdom&quot;&gt;jsdom&lt;/a&gt; from the previous post to end-to-end tests, reducing the number of mocks we need to maintain.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As usual, the code is &lt;a href=&quot;https://github.com/jdleesmiller/todo-demo&quot;&gt;available on GitHub&lt;/a&gt;, and each post in the series has a git tag that marks the &lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/tree/todo-end-to-end-jsdom/todo&quot;&gt;corresponding code&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id=&quot;separate-test-and-development-environments&quot;&gt;Separate Test and Development Environments&lt;/h3&gt;

&lt;p&gt;The main idea of end-to-end testing is to stand up the whole system, put it into a given state, and then run automatic tests against it. In most cases, this means wiping out the state in the database before each test run, so we’d rather not run them against a development environment used for more manual or exploratory testing. Instead, we’d like to have separate development and test environments.&lt;/p&gt;

&lt;p&gt;The simplest approach is to have two completely separate environments, but this can be expensive. For a large application, just running two copies of every service can tax your development machine. And we have to keep these two environments in sync in terms of installed packages and code. Sharing resources between the two environments can help with these problems.&lt;/p&gt;

&lt;p&gt;In this case, the shared resources will be the application’s postgres server (with separate logical databases for development and test), to save having to run two full postgres instances, and its &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node_modules&lt;/code&gt; folders, to keep dependencies in sync between development and test environments. Of course, in a larger application, there might be more that could be shared. In diagram form, what we’re aiming for is:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;/assets/todo-demo/todo-demo-shared.svg&quot;&gt;&lt;img src=&quot;/assets/todo-demo/todo-demo-shared.svg&quot; alt=&quot;TODO&quot; style=&quot;max-width: 448px;&quot; /&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;Notably:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;The development and test environments each have their own network and on it their own instances of the application’s services. This means that development and test services can use the same names in both environments without collisions, which reduces the amount of configuration that has to differ between the environments.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The postgres database server lives in a third shared environment, with a presence on both development and test networks, so both development and test applications can talk to the same database server (but use different logical databases).&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;For faster edit-test cycles, and to keep code in sync between development and test environments, both the development and test services need to have the source code on the host bind mounted in, which means two instances of the ‘&lt;a href=&quot;/articles/2019/09/06/lessons-building-node-app-docker.html&quot;&gt;node modules trick&lt;/a&gt;’ with a nested &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node_modules&lt;/code&gt; volume per service. To keep the dependencies in sync, both the development and test instance of each service mount the same shared &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node_modules&lt;/code&gt; volume. That way changes to packages in the development environment are automatically reflected in the test environment.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To set this up, we use several Docker Compose features: project names, variable substitution, Compose file merging, and external links and volumes.&lt;/p&gt;

&lt;h4 id=&quot;project-names&quot;&gt;Project Names&lt;/h4&gt;

&lt;p&gt;Compose prepends a &lt;a href=&quot;https://docs.docker.com/compose/reference/envvars/#compose_project_name&quot;&gt;project name&lt;/a&gt; to all of the resources that it creates in Docker so that they don’t conflict with those of other projects. This is why, in the last post, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;backend&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;frontend&lt;/code&gt; services in the Compose file produced containers called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;todo_backend_1&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;todo_frontend_1&lt;/code&gt;&lt;sup id=&quot;fnref:suffix&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:suffix&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;:&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ docker-compose ps
Name                    Command               State           Ports
---------------------------------------------------------------------------------
todo_backend_1    docker-entrypoint.sh npx n ...   Up
todo_frontend_1   docker-entrypoint.sh npx w ...   Up      0.0.0.0:8080-&amp;gt;8080/tcp
todo_postgres_1   docker-entrypoint.sh postgres    Up      5432/tcp
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;By default, Compose uses the basename of its working directory, but this can be overridden with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--project-name&lt;/code&gt; flag. Here we’ll use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--project-name todo_development&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--project-name todo_test&lt;/code&gt; to create two projects for the development and test environments, respectively. We’ll also create a third project for the shared environment just called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;todo&lt;/code&gt;.&lt;/p&gt;

&lt;h4 id=&quot;variable-substitutions-and-merging-compose-files&quot;&gt;Variable Substitutions and Merging Compose Files&lt;/h4&gt;

&lt;p&gt;Running the same Compose file with different project names lets us create isolated and identical environments, but usually what we actually want is &lt;em&gt;nearly&lt;/em&gt;-identical environments with small differences. For example, the development and test environments should be as similar as possible, but we need them to use different logical databases. Compose provides two useful features that enable this.&lt;/p&gt;

&lt;p&gt;Firstly, for single-value changes, we can use &lt;a href=&quot;https://docs.docker.com/compose/compose-file/#variable-substitution&quot;&gt;variable substitution&lt;/a&gt; to inject environment variables from the host into the Compose file. Here we’ll use an environment variable called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ENV&lt;/code&gt; to control the database name in the database connection string, for example.&lt;/p&gt;

&lt;p&gt;Secondly, for larger structural changes to Compose files, the &lt;a href=&quot;https://docs.docker.com/compose/reference/overview/#specifying-multiple-compose-files&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--file&lt;/code&gt; flag&lt;/a&gt; lets us load multiple Compose files, which Compose intelligently merges together. Here we’ll have a root Compose file with the shared configuration for the development and test environments, and then an environment-specific Compose file for each environment.&lt;/p&gt;

&lt;h4 id=&quot;external-networks-links-and-volumes&quot;&gt;External Networks, Links and Volumes&lt;/h4&gt;

&lt;p&gt;To allow Compose to find resources defined in other environments, we’ll use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;external&lt;/code&gt; and &lt;a href=&quot;https://docs.docker.com/compose/compose-file/#external_links&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;external_links&lt;/code&gt;&lt;/a&gt; properties in the Compose file. The development and test projects will refer to &lt;a href=&quot;https://docs.docker.com/compose/compose-file/#external-1&quot;&gt;networks&lt;/a&gt; and &lt;a href=&quot;https://docs.docker.com/compose/compose-file/#external&quot;&gt;volumes&lt;/a&gt; in the shared project using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;external&lt;/code&gt;, and the services’ link to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;postgres&lt;/code&gt; database container is an external link.&lt;/p&gt;

&lt;p&gt;Putting this all together, we need three new Compose files and some modifications to our existing Compose file from &lt;a href=&quot;/articles/2020/01/12/testing-node-docker-compose-frontend.html&quot;&gt;part 2&lt;/a&gt;, which we’ll take in turn. First, there’s the compose file for the shared environment, which sets up the networks, the postgres database, and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node_modules&lt;/code&gt; volumes.&lt;/p&gt;

&lt;h4 id=&quot;docker-composesharedyml&quot;&gt;&lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-envs/todo/docker-compose.shared.yml&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose.shared.yml&lt;/code&gt;&lt;/a&gt;&lt;/h4&gt;

&lt;div class=&quot;language-yml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;3.7&apos;&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;networks&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;development_default&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;test_default&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;postgres&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;postgres&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;POSTGRES_HOST_AUTH_METHOD&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;trust&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;networks&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;development_default&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;test_default&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;backend_node_modules&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;frontend_node_modules&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We’ll run this &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose.shared.yml&lt;/code&gt; Compose file with the project name set to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;todo&lt;/code&gt;, so the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;development_default&lt;/code&gt; network here will have the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;todo&lt;/code&gt; prefix added, yielding &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;todo_development_default&lt;/code&gt; as its full name in Docker. The test network will similarly end up being called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;todo_test_default&lt;/code&gt;. The shared &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;postgres&lt;/code&gt; sits on both networks.&lt;/p&gt;

&lt;p&gt;The volumes similarly pick up this &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;todo&lt;/code&gt; prefix and so end up being called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;todo_backend_node_modules&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;todo_frontend_node_modules&lt;/code&gt; in Docker.&lt;/p&gt;

&lt;p&gt;The main &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose.yml&lt;/code&gt; file then has to change to reference these shared resources, as follows:&lt;/p&gt;

&lt;h4 id=&quot;docker-composeyml&quot;&gt;&lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-envs/todo/docker-compose.yml&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose.yml&lt;/code&gt;&lt;/a&gt;&lt;/h4&gt;

&lt;div class=&quot;language-diff highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;gh&quot;&gt;diff --git a/todo/docker-compose.yml b/todo/docker-compose.yml
index 37983ad..69fbc29 100644
&lt;/span&gt;&lt;span class=&quot;gd&quot;&gt;--- a/todo/docker-compose.yml
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+++ b/todo/docker-compose.yml
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;@@ -1,15 +1,22 @@&lt;/span&gt;
 version: &apos;3.7&apos;

+# Use the network set up in the shared compose file.
&lt;span class=&quot;gi&quot;&gt;+networks:
+  default:
+    external: true
+    name: todo_${ENV}_default
+
&lt;/span&gt; services:
   backend:
     build:
       context: .
       target: development-backend
     command: npx nodemon server.js
&lt;span class=&quot;gd&quot;&gt;-    depends_on:
-      - postgres
&lt;/span&gt;     environment:
&lt;span class=&quot;gi&quot;&gt;+      DATABASE_URL: postgres://postgres:postgres@postgres/${ENV}
&lt;/span&gt;       PORT: 8080
&lt;span class=&quot;gi&quot;&gt;+    external_links:
+      - todo_postgres_1:postgres
&lt;/span&gt;     volumes:
       - ./backend:/srv/todo/backend
       - backend_node_modules:/srv/todo/backend/node_modules
&lt;span class=&quot;p&quot;&gt;@@ -24,17 +31,15 @@&lt;/span&gt; services:
     environment:
       HOST: frontend
       PORT: 8080
&lt;span class=&quot;gd&quot;&gt;-    ports:
-      - &apos;8080:8080&apos;
&lt;/span&gt;     volumes:
       - ./frontend:/srv/todo/frontend
       - frontend_node_modules:/srv/todo/frontend/node_modules

-  postgres:
&lt;span class=&quot;gd&quot;&gt;-    image: postgres:12
-    environment:
-      POSTGRES_HOST_AUTH_METHOD: trust
-
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+# Use the node_modules volumes set up in the shared compose file.
&lt;/span&gt; volumes:
   backend_node_modules:
&lt;span class=&quot;gi&quot;&gt;+    name: todo_backend_node_modules
+    external: true
&lt;/span&gt;   frontend_node_modules:
&lt;span class=&quot;gi&quot;&gt;+    name: todo_frontend_node_modules
+    external: true
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The main changes here are to:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;Redefine the default network as external and point it to the appropriate network from the shared environment. Here we use our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ENV&lt;/code&gt; environment variable, which is set to either &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;development&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;test&lt;/code&gt;, and Compose variable substitution to switch between the two networks. Our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin&lt;/code&gt; scripts will handle setting this &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ENV&lt;/code&gt; variable, as we’ll see shortly.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Remove the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;postgres&lt;/code&gt; service and instead point the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;backend&lt;/code&gt; service at the shared postgres service using an external link. The external link has to refer to the full container name, which due to the shared environment’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;todo&lt;/code&gt; prefix turns out to be &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;todo_postgres_1&lt;/code&gt;; however, we alias it to just &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;postgres&lt;/code&gt; within the current environment, so nothing else needs to know about this. Removing the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;postgres&lt;/code&gt; service also means that we have to remove the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;backend&lt;/code&gt; service’s dependency on it; instead we have to take responsibility for making sure that postgres starts up. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin&lt;/code&gt; scripts will again help with this.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Mark the named &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node_modules&lt;/code&gt; volumes as external and point them at the shared volumes.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Stop exposing port &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;8080&lt;/code&gt; on the host for the frontend by default. We’re going to run the development and test environments at the same time, so they can’t both expose the same port.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This brings us to our third and fourth Compose files, which are specific to the development and test environments, and are fortunately very short. The development Compose file lets us expose &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;8080&lt;/code&gt; for the frontend service only in the development environment:&lt;/p&gt;

&lt;h4 id=&quot;docker-composedevelopmentyml&quot;&gt;&lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-envs/todo/docker-compose.development.yml&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose.development.yml&lt;/code&gt;&lt;/a&gt;&lt;/h4&gt;

&lt;div class=&quot;language-yml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;3.7&apos;&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;frontend&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;8080:8080&apos;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The test Compose file doesn’t (yet) have anything to add on top of the common configuration from the root &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose.yml&lt;/code&gt;, so it is empty but for its Compose version number:&lt;/p&gt;

&lt;h4 id=&quot;docker-composetestyml&quot;&gt;&lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-envs/todo/docker-compose.test.yml&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose.test.yml&lt;/code&gt;&lt;/a&gt;&lt;/h4&gt;

&lt;div class=&quot;language-yml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;3.7&apos;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;the-bin-scripts&quot;&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin&lt;/code&gt; Scripts&lt;/h3&gt;

&lt;p&gt;At this point you may be thinking that it would be a pain to list out all these Compose files and environment variables on the command line, and you would be right. It’s no longer practical to run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose&lt;/code&gt; directly — instead we will run it only through helper scripts.&lt;/p&gt;

&lt;p&gt;The first of these scripts is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/dc&lt;/code&gt;, which wraps the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose&lt;/code&gt; command so it takes as its first argument the environment we want it to run in:&lt;/p&gt;

&lt;h4 id=&quot;bindc&quot;&gt;&lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-envs/todo/bin/dc&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/dc&lt;/code&gt;&lt;/a&gt;&lt;/h4&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;#!/usr/bin/env bash&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;#&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Run docker-compose in the given environment.&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;#&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Usage: bin/dc &amp;lt;d[evelopment]|t[est]|s[shared]&amp;gt; &amp;lt;arguments for docker-compose&amp;gt;&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;#&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;source&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;BASH_SOURCE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;%/*&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/.helpers.sh&quot;&lt;/span&gt;

docker_compose_in_env &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$@&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;It supports abbreviations, so &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/dc d ps&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/dc development ps&lt;/code&gt; will both run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose ps&lt;/code&gt; in the development environment.&lt;/p&gt;

&lt;p&gt;The lynchpin of the whole approach is the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker_compose_in_env&lt;/code&gt; function, which is defined in helper file so other &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin&lt;/code&gt; scripts can use it, so let’s look at that:&lt;/p&gt;

&lt;h4 id=&quot;binhelperssh&quot;&gt;&lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-envs/todo/bin/.helpers.sh&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/.helpers.sh&lt;/code&gt;&lt;/a&gt;&lt;/h4&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-e&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Run docker-compose in the given environment.&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;function &lt;/span&gt;docker_compose_in_env &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;local &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;ENV&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;get_full_env_name &lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$ENV&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in
  &lt;/span&gt;development &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;test&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;ENV&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$ENV&lt;/span&gt; docker-compose &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;--project-name&lt;/span&gt; todo_&lt;span class=&quot;nv&quot;&gt;$ENV&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;--file&lt;/span&gt; docker-compose.yml &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;--file&lt;/span&gt; docker-compose.&lt;span class=&quot;nv&quot;&gt;$ENV&lt;/span&gt;.yml &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
      &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;@&lt;/span&gt;:2&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;;;&lt;/span&gt;
  shared &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    docker-compose &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;--project-name&lt;/span&gt; todo &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;--file&lt;/span&gt; docker-compose.shared.yml &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;@&lt;/span&gt;:2&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;;;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Unexpected environment name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;exit &lt;/span&gt;1 &lt;span class=&quot;p&quot;&gt;;;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;esac&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Get the full environment name, allowing shorthand.&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;function &lt;/span&gt;get_full_env_name &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in
    &lt;/span&gt;d &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt; development &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;echo &lt;/span&gt;development &lt;span class=&quot;p&quot;&gt;;;&lt;/span&gt;
    t &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;test&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;echo test&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;;;&lt;/span&gt;
    s &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt; shared &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;echo &lt;/span&gt;shared &lt;span class=&quot;p&quot;&gt;;;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Expected environment d[evelopment]|t[est]|s[hared]&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;exit &lt;/span&gt;1 &lt;span class=&quot;p&quot;&gt;;;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;esac&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In the development and test environments, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker_compose_in_env&lt;/code&gt; function&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;sets the Compose project name to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;todo_development&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;todo_test&lt;/code&gt;,&lt;/li&gt;
  &lt;li&gt;sets the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ENV&lt;/code&gt; variable to either &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;development&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;test&lt;/code&gt; for Compose to use for variable substitutions, and&lt;/li&gt;
  &lt;li&gt;loads both the root &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose.yml&lt;/code&gt; Compose file and the appropriate environment-specific Compose file.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It then passes the rest of its arguments on to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose&lt;/code&gt; with a somewhat cryptic &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;${@:2}&quot;&lt;/code&gt;, which makes sense when you know that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$@&lt;/code&gt; expands to all arguments, and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:2&lt;/code&gt; says we should drop the first one, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$1&lt;/code&gt;, which is the environment name.&lt;/p&gt;

&lt;p&gt;The shared environment is simpler: it runs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose&lt;/code&gt; with the project name set to just &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;todo&lt;/code&gt; &lt;sup id=&quot;fnref:project-name&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:project-name&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;2&lt;/a&gt;&lt;/sup&gt; and loads the shared Compose file, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose.shared.yml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Another important script that uses the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker_compose_in_env&lt;/code&gt; function is the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/up&lt;/code&gt; script, which is responsible for starting up the environments. It requires the following changes, compared to last time with a single environment:&lt;/p&gt;

&lt;h4 id=&quot;binup&quot;&gt;&lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-envs/todo/bin/up&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/up&lt;/code&gt;&lt;/a&gt;&lt;/h4&gt;

&lt;div class=&quot;language-diff highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;gh&quot;&gt;diff --git a/todo/bin/up b/todo/bin/up
index 3fa0a0e..d4d6d75 100755
&lt;/span&gt;&lt;span class=&quot;gd&quot;&gt;--- a/todo/bin/up
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+++ b/todo/bin/up
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;@@ -1,20 +1,20 @@&lt;/span&gt;
 #!/usr/bin/env bash

-set -e
&lt;span class=&quot;gi&quot;&gt;+source &quot;${BASH_SOURCE%/*}/.helpers.sh&quot;
&lt;/span&gt;
-docker-compose up -d postgres
&lt;span class=&quot;gi&quot;&gt;+docker_compose_in_env shared up -d
&lt;/span&gt;
 WAIT_FOR_PG_ISREADY=&quot;while ! pg_isready --quiet; do sleep 1; done;&quot;
&lt;span class=&quot;gd&quot;&gt;-docker-compose exec postgres bash -c &quot;$WAIT_FOR_PG_ISREADY&quot;
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+docker_compose_in_env shared exec postgres bash -c &quot;$WAIT_FOR_PG_ISREADY&quot;
&lt;/span&gt;
 for ENV in development test
 do
   # Create database for this environment if it doesn&apos;t already exist.
&lt;span class=&quot;gd&quot;&gt;-  docker-compose exec postgres \
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+  docker_compose_in_env shared exec postgres \
&lt;/span&gt;     su - postgres -c &quot;psql $ENV -c &apos;&apos; || createdb $ENV&quot;

   # Run migrations in this environment.
&lt;span class=&quot;gd&quot;&gt;-  docker-compose run --rm -e NODE_ENV=$ENV backend npx knex migrate:latest
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+  docker_compose_in_env $ENV run --rm backend npx knex migrate:latest
&lt;/span&gt; done

-docker-compose up -d
&lt;span class=&quot;gi&quot;&gt;+docker_compose_in_env development up -d
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The script essentially does the same things, but it runs the database-related commands in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shared&lt;/code&gt; environment, and the migrations in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;development&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;test&lt;/code&gt; environment, as appropriate.&lt;/p&gt;

&lt;p&gt;Other useful &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin&lt;/code&gt; scripts include &lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-envs/todo/bin/test&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/test&lt;/code&gt;&lt;/a&gt; to run all the tests (now in the test environment), &lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-envs/todo/bin/stop&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/stop&lt;/code&gt;&lt;/a&gt; to stop all the containers, and &lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-envs/todo/bin/down&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/down&lt;/code&gt;&lt;/a&gt; to tear things down (now optionally preserving the data in the shared environment).&lt;/p&gt;

&lt;h3 id=&quot;the-end-to-end-test&quot;&gt;The End-To-End Test&lt;/h3&gt;

&lt;p&gt;Now that we have a separate test environment, we’re nearly ready to add an end-to-end test into that environment. The final bits of preparation are to separate out the model layer from the backend into its own package, here called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;storage&lt;/code&gt;, and then to write an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;end-to-end-test&lt;/code&gt; package to contain the end-to-end test and its dependencies.&lt;/p&gt;

&lt;p&gt;The reason to extract a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;storage&lt;/code&gt; package is that both the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;backend&lt;/code&gt; service and the end-to-end test will need to access the database, and that will be easier if we can share the database-related code between them. Our TODO application is actually simple enough that we could test everything using only public interfaces, using a &lt;a href=&quot;https://en.wikipedia.org/wiki/Black-box_testing&quot;&gt;black box&lt;/a&gt; approach. However, in more complicated applications, end-to-end tests often benefit from a more grey box approach — at minimum they need to be able to efficiently reset the database state in between tests, and it is helpful to be able to set up more complicated test scenarios by using the model layer directly.&lt;/p&gt;

&lt;p&gt;The &lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/commit/89212a42b3760dd01d3d5f78b21ebe78cdf72199&quot;&gt;full diff&lt;/a&gt; for splitting out the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;storage&lt;/code&gt; package is long, but essentially it moves the database dependencies, configuration, migrations and test helpers, and our model class, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Task&lt;/code&gt;, into &lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-storage/todo/storage&quot;&gt;the new package&lt;/a&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ tree storage
storage
├── index.js
├── knexfile.js
├── migrations
│   └── 20190720190344_create_tasks.js
├── node_modules
├── package-lock.json
├── package.json
├── src
│   ├── knex.js
│   └── task.js
└── test
    └── support
        ├── cleanup.js
        └── knex-hook.js
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;backend&lt;/code&gt; service’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;package.json&lt;/code&gt; then gains a &lt;a href=&quot;https://docs.npmjs.com/files/package.json#local-paths&quot;&gt;local path dependency&lt;/a&gt; on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;storage&lt;/code&gt; package, instead of depending on the database packages directly:&lt;/p&gt;

&lt;h4 id=&quot;backendpackagejson&quot;&gt;&lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-storage/todo/backend/package.json&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;backend/package.json&lt;/code&gt;&lt;/a&gt;&lt;/h4&gt;

&lt;div class=&quot;language-diff highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;gh&quot;&gt;diff --git a/todo/backend/package.json b/todo/backend/package.json
index 36855a1..40c2363 100644
&lt;/span&gt;&lt;span class=&quot;gd&quot;&gt;--- a/todo/backend/package.json
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+++ b/todo/backend/package.json
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;@@ -13,9 +13,7 @@&lt;/span&gt;
   &quot;dependencies&quot;: {
     &quot;body-parser&quot;: &quot;^1.19.0&quot;,
     &quot;express&quot;: &quot;^4.17.1&quot;,
&lt;span class=&quot;gd&quot;&gt;-    &quot;knex&quot;: &quot;^0.19.5&quot;,
-    &quot;objection&quot;: &quot;^1.6.11&quot;,
-    &quot;pg&quot;: &quot;^7.12.1&quot;
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+    &quot;storage&quot;: &quot;file:../storage&quot;
&lt;/span&gt;   },
   &quot;devDependencies&quot;: {
     &quot;mocha&quot;: &quot;^6.2.0&quot;,
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The local path dependency lets us update the storage package locally without having to publish it to npm every time, which would be extremely tedious. This approach also requires the usual changes to the &lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-storage/todo/Dockerfile&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt;&lt;/a&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm install&lt;/code&gt; the package and changes to the &lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-storage/todo/docker-compose.yml&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose.yml&lt;/code&gt;&lt;/a&gt; file to bind mount the source for the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;storage&lt;/code&gt; package into the containers that need it.&lt;/p&gt;

&lt;p&gt;Now we can create the &lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/tree/todo-end-to-end-test/todo/end-to-end-test&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;end-to-end-test&lt;/code&gt;&lt;/a&gt; package that also depends on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;storage&lt;/code&gt; package and declares our other test dependencies, which are here &lt;a href=&quot;https://mochajs.org/&quot;&gt;mocha&lt;/a&gt; to drive the test and &lt;a href=&quot;https://pptr.dev/&quot;&gt;puppeteer&lt;/a&gt; to drive the headless browser that will exercise the frontend. Its &lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-end-to-end-test/todo/end-to-end-test/Dockerfile&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt;&lt;/a&gt; requires some &lt;a href=&quot;https://github.com/puppeteer/puppeteer/blob/master/docs/troubleshooting.md#running-puppeteer-in-docker&quot;&gt;specific setup&lt;/a&gt; to allow run puppeteer in a container, but it is well documented.&lt;/p&gt;

&lt;p&gt;So, finally, here is the end-to-end test:&lt;/p&gt;

&lt;h4 id=&quot;end-to-end-testtesttodotestjs&quot;&gt;&lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-end-to-end-test/todo/end-to-end-test/test/todo.test.js&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;end-to-end-test/test/todo.test.js&lt;/code&gt;&lt;/a&gt;&lt;/h4&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;assert&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;puppeteer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;puppeteer&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Task&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;storage&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;cleanup&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;storage/test/support/cleanup&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;BASE_URL&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;process&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;BASE_URL&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;before&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;global&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;browser&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;puppeteer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;launch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;executablePath&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;google-chrome-unstable&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;headless&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;process&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;PUPPETEER_HEADLESS&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;after&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;global&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;browser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;close&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;beforeEach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;cleanup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;database&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;describe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;TO DO&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;timeout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;beforeEach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;insert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;foo&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;bar&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}])&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;lists, creates and completes tasks&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;page&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;global&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;browser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;newPage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;goto&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;BASE_URL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;waitForNumberOfTasksToBe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;tasks&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;$$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;.todo-task&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// Complete task foo.&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;assert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;strictEqual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;getTaskText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;tasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]),&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;foo&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;complete&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;tasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;button&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;complete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;click&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;waitForNumberOfTasksToBe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// Create a new task, baz.&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;.todo-new-task input[type=text]&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;baz&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;click&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;.todo-new-task button&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;waitForSelector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;.todo-task button&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;waitForNumberOfTasksToBe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// Complete task baz.&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;tasks&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;$$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;.todo-task&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;assert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;strictEqual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;getTaskText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;tasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]),&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;baz&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;complete&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;tasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;button&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;complete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;click&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;waitForNumberOfTasksToBe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// Only bar should remain.&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;tasks&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;$$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;.todo-task&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;assert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;strictEqual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;getTaskText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;tasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]),&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;bar&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;waitForNumberOfTasksToBe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;waitForFunction&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;s2&quot;&gt;`document.querySelectorAll(&quot;.todo-task&quot;).length == &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;getTaskText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;elementHandle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;elementHandle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;$eval&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;span&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;node&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;innerText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Key points:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;A global puppeteer instance, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;global.browser&lt;/code&gt; is shared by all the tests (though here there’s only one test).&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BASE_URL&lt;/code&gt; environment variable points it at the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;frontend&lt;/code&gt; container (in the test environment), which proxies requests from the browser through to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;backend&lt;/code&gt; container, as discussed in the &lt;a href=&quot;/articles/2020/01/12/testing-node-docker-compose-frontend.html&quot;&gt;previous post&lt;/a&gt;.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Before each test, it uses functionality from the shared &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;storage&lt;/code&gt; package to first reset the database state, with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cleanup.database&lt;/code&gt;, and then seed the database with two starter &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Task&lt;/code&gt;s, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;foo&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bar&lt;/code&gt;.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The test itself is based mainly on query selectors. For example, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;page.$$(&apos;.todo-task&apos;)&lt;/code&gt; finds all of the elements on the page with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;todo-task&lt;/code&gt; CSS class, which are the list items for the tasks, as per &lt;a href=&quot;/articles/2020/01/12/testing-node-docker-compose-frontend.html#frontendsrccomponenttaskjs&quot;&gt;the frontend&lt;/a&gt;. More on this later.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;waitForNumberOfTasksToBe&lt;/code&gt; helper function uses puppeteer’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;waitForFunction&lt;/code&gt; primitive to poll until the given number of tasks are on the page. This is the main way in which the test handles the asynchronous loading and rendering of data.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, to run the test, we need to tell Compose how to start a container for it. To do this we create a separate Compose file that we’ll merge together with the other Compose files using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--file&lt;/code&gt; flag discussed above:&lt;/p&gt;

&lt;h4 id=&quot;docker-composeend-to-end-testyml&quot;&gt;&lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-end-to-end-test/todo/docker-compose.end-to-end-test.yml&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose.end-to-end-test.yml&lt;/code&gt;&lt;/a&gt;&lt;/h4&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;3.7&apos;&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;end-to-end-test&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;.&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;dockerfile&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;end-to-end-test/Dockerfile&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;cap_add&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;SYS_ADMIN&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# for puppeteer&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;npm test&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;depends_on&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;frontend&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;DATABASE_URL&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;postgres://postgres:postgres@postgres/test&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;BASE_URL&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;http://frontend:8080&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;./end-to-end-test:/srv/todo/end-to-end-test&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;end_to_end_test_node_modules:/srv/todo/end-to-end-test/node_modules&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;./storage:/srv/todo/storage&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;storage_node_modules:/srv/todo/storage/node_modules&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;end_to_end_test_node_modules&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;storage_node_modules&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;todo_storage_node_modules&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;external&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Notably:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;This Compose file defines an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;end-to-end-test&lt;/code&gt; ‘service’ that we’ll only ever run as a one-off command, with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose run&lt;/code&gt; instead of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose up&lt;/code&gt;.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;end-to-end-test&lt;/code&gt; service defined here depends on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;frontend&lt;/code&gt; service, which is defined in the main Compose file. It therefore depends on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;backend&lt;/code&gt; service too, through the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;frontend&lt;/code&gt;. Compose will automatically bring up all the application services required for the end-to-end test. It won’t automatically bring up the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;postgres&lt;/code&gt; service in the shared environment, however, because it’s external — we have to run the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/up&lt;/code&gt; script to make it work.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;It sets the test database URL and the base URL where it can find the frontend as environment variables.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Like other node services, the source is bind mounted into the container, and we again apply the &lt;a href=&quot;/articles/2019/09/06/lessons-building-node-app-docker.html&quot;&gt;node modules trick&lt;/a&gt;.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, we’ve again reached a point in this series where we can run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm test&lt;/code&gt; in a container:&lt;/p&gt;
&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;bin/up
bin/dc t &lt;span class=&quot;nt&quot;&gt;--file&lt;/span&gt; docker-compose.end-to-end-test.yml run &lt;span class=&quot;nt&quot;&gt;--rm&lt;/span&gt; end-to-end-test
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;This runs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose&lt;/code&gt; in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;test&lt;/code&gt; environment (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;t&lt;/code&gt;) with our extra Compose file and runs the tests in the container. It’s a lot to type, so we’ll usually run it through the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/test&lt;/code&gt; script, which runs all the tests:&lt;/p&gt;

&lt;h4 id=&quot;bintest&quot;&gt;&lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-end-to-end-test/todo/bin/test&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/test&lt;/code&gt;&lt;/a&gt;&lt;/h4&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;#!/usr/bin/env bash&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;source&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;BASH_SOURCE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;%/*&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/.helpers.sh&quot;&lt;/span&gt;

docker_compose_in_env &lt;span class=&quot;nb&quot;&gt;test &lt;/span&gt;run &lt;span class=&quot;nt&quot;&gt;--rm&lt;/span&gt; backend npm &lt;span class=&quot;nb&quot;&gt;test&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$@&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
docker_compose_in_env &lt;span class=&quot;nb&quot;&gt;test &lt;/span&gt;run &lt;span class=&quot;nt&quot;&gt;--rm&lt;/span&gt; frontend npm &lt;span class=&quot;nb&quot;&gt;test&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$@&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
docker_compose_in_env &lt;span class=&quot;nb&quot;&gt;test&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--file&lt;/span&gt; docker-compose.end-to-end-test.yml &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  run &lt;span class=&quot;nt&quot;&gt;--rm&lt;/span&gt; end-to-end-test
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The last line runs the end-to-end tests:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ bin/up
...
$ bin/test
...
Starting todo_test_backend_1 ... done
Starting todo_test_frontend_1 ... done

&amp;gt; end-to-end-test@1.0.0 test /srv/todo/end-to-end-test
&amp;gt; mocha --timeout 10000



  TO DO
    ✓ lists, creates and completes tasks (1724ms)


  1 passing (2s)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;revisiting-a-frontend-integration-test&quot;&gt;Revisiting a Frontend Integration Test&lt;/h3&gt;

&lt;p&gt;In the &lt;a href=&quot;https://jdlm.info/articles/2020/01/12/testing-node-docker-compose-frontend.html#integration-tests&quot;&gt;previous post&lt;/a&gt; about frontend testing, we had a frontend integration test running with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jsdom&lt;/code&gt; that made fairly heavy use of request mocking to simulate responses from the backend. Now that we have the infrastructure for end-to-end tests, we have the option of just letting the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jsdom&lt;/code&gt; tests talk to the backend, so let’s see how that looks.&lt;/p&gt;

&lt;p&gt;The main &lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/commit/60be4f35e05e6d52acd0ab2df05138a9d5ed26c5&quot;&gt;change required&lt;/a&gt; is that the frontend tests (but not the frontend itself) also needs to load the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;storage&lt;/code&gt; package and to be able to talk to the database and the backend. We can add the required config to the test environment Compose file that we stubbed out earlier:&lt;/p&gt;

&lt;h4 id=&quot;docker-composetestyml-1&quot;&gt;&lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-end-to-end-jsdom/todo/docker-compose.test.yml&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose.test.yml&lt;/code&gt;&lt;/a&gt;&lt;/h4&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;3.7&apos;&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;frontend&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;DATABASE_URL&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;postgres://postgres:postgres@postgres/test&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;BASE_URL&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;http://backend:8080&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then we can remove the mocking from the frontend integration test, which makes it much more succinct:&lt;/p&gt;

&lt;h4 id=&quot;frontendtestintegrationtodotestjs&quot;&gt;&lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-end-to-end-jsdom/todo/frontend/test/integration/todo.test.js&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;frontend/test/integration/todo.test.js&lt;/code&gt;&lt;/a&gt;&lt;/h4&gt;

&lt;div class=&quot;language-diff highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;gh&quot;&gt;diff --git a/todo/frontend/test/integration/todo.test.js b/todo/frontend/test/integration/todo.test.js
index bb0d6dd..bd0a56e 100644
&lt;/span&gt;&lt;span class=&quot;gd&quot;&gt;--- a/todo/frontend/test/integration/todo.test.js
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+++ b/todo/frontend/test/integration/todo.test.js
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;@@ -1,23 +1,23 @@&lt;/span&gt;
&lt;span class=&quot;gi&quot;&gt;+import assert from &apos;assert&apos;
&lt;/span&gt; import React from &apos;react&apos;
 import {
&lt;span class=&quot;gd&quot;&gt;-  cleanup,
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+  cleanup as cleanupReactTest,
&lt;/span&gt;   fireEvent,
   render,
   waitForElement,
   waitForElementToBeRemoved
 } from &apos;@testing-library/react&apos;

-import fetchMock from &apos;../support/fetch-mock&apos;
&lt;span class=&quot;gi&quot;&gt;+import { Task } from &apos;storage&apos;
+import { database as cleanupDatabase } from &apos;storage/test/support/cleanup&apos;
+
&lt;/span&gt; import App from &apos;../../src/component/app&apos;

 describe(&apos;TO DO App&apos;, function() {
&lt;span class=&quot;gd&quot;&gt;-  afterEach(cleanup)
-  afterEach(fetchMock.reset)
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+  beforeEach(cleanupDatabase)
+  afterEach(cleanupReactTest)
&lt;/span&gt;
   it(&apos;lists, creates and completes tasks&apos;, async function() {
&lt;span class=&quot;gd&quot;&gt;-    // Load empty list.
-    fetchMock.getOnce(&apos;path:/api/tasks&apos;, { tasks: [] })
-
&lt;/span&gt;     const { getByText, getByLabelText } = render(&amp;lt;App /&amp;gt;)

     const description = getByLabelText(&apos;new task description&apos;)
&lt;span class=&quot;p&quot;&gt;@@ -25,49 +25,24 @@&lt;/span&gt; describe(&apos;TO DO App&apos;, function() {

     await waitForElementToBeRemoved(() =&amp;gt; getByText(/loading/i))

-    // Create &apos;find keys&apos; task.
&lt;span class=&quot;gd&quot;&gt;-    fetchMock.postOnce(&apos;path:/api/tasks&apos;, {
-      task: { id: 1, description: &apos;find keys&apos; }
-    })
-    fetchMock.getOnce(&apos;path:/api/tasks&apos;, {
-      tasks: [{ id: 1, description: &apos;find keys&apos; }]
-    })
&lt;/span&gt;     fireEvent.change(description, { target: { value: &apos;find keys&apos; } })
     fireEvent.click(addTask)

     await waitForElement(() =&amp;gt; getByText(&apos;find keys&apos;))

-    // Create &apos;buy milk&apos; task.
&lt;span class=&quot;gd&quot;&gt;-    fetchMock.postOnce(&apos;path:/api/tasks&apos;, {
-      task: { id: 2, description: &apos;buy milk&apos; }
-    })
-    fetchMock.getOnce(&apos;path:/api/tasks&apos;, {
-      tasks: [
-        { id: 1, description: &apos;find keys&apos; },
-        { id: 2, description: &apos;buy milk&apos; }
-      ]
-    })
&lt;/span&gt;     fireEvent.change(description, { target: { value: &apos;buy milk&apos; } })
     fireEvent.click(addTask)

     await waitForElement(() =&amp;gt; getByText(&apos;buy milk&apos;))

-    // Complete &apos;buy milk&apos; task.
&lt;span class=&quot;gd&quot;&gt;-    fetchMock.deleteOnce(&apos;path:/api/tasks/2&apos;, 204)
-    fetchMock.getOnce(&apos;path:/api/tasks&apos;, {
-      tasks: [{ id: 1, description: &apos;find keys&apos; }]
-    })
-
&lt;/span&gt;     fireEvent.click(getByLabelText(&apos;mark buy milk complete&apos;))

     await waitForElementToBeRemoved(() =&amp;gt; getByText(&apos;buy milk&apos;))

-    // Complete &apos;find keys&apos; task.
&lt;span class=&quot;gd&quot;&gt;-    fetchMock.deleteOnce(&apos;path:/api/tasks/1&apos;, 204)
-    fetchMock.getOnce(&apos;path:/api/tasks&apos;, { tasks: [] })
-
&lt;/span&gt;     fireEvent.click(getByLabelText(&apos;mark find keys complete&apos;))

     await waitForElementToBeRemoved(() =&amp;gt; getByText(&apos;find keys&apos;))
&lt;span class=&quot;gi&quot;&gt;+
+    assert.strictEqual(await Task.query().resultSize(), 0)
&lt;/span&gt;   })
 })
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Compared to the fully end-to-end test we wrote with puppeteer above, which depended on a lot of CSS classes to make the query selectors work, this &lt;em&gt;mostly&lt;/em&gt; end-to-end test with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jsdom&lt;/code&gt; uses React Test Library matchers that are hopefully much less fragile and more closely resemble how the user uses the application, improving the fidelity of the test &lt;sup id=&quot;fnref:pptr-testing-library&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:pptr-testing-library&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;. One disadvantage is that we &lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/commit/517f458b402f4d028dfe06ca43905a8f5deff38b&quot;&gt;lose&lt;/a&gt; the ability to run the frontend integration test in a normal browser, where loading the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;storage&lt;/code&gt; package is impossible, but we could still run the rest of the frontend tests in a normal browser. End-to-end testing with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jsdom&lt;/code&gt; instead of a full headless browser is worth considering for many applications.&lt;/p&gt;

&lt;h3 id=&quot;conclusions&quot;&gt;Conclusions&lt;/h3&gt;

&lt;p&gt;In this post we’ve added end-to-end tests to our example TODO list application. We’ve seen how to:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Create separate test and development environments with Docker Compose, while still sharing some resources between environments for efficiency.&lt;/li&gt;
  &lt;li&gt;Write scripts to help manage the more complicated &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose&lt;/code&gt; commands required for this approach.&lt;/li&gt;
  &lt;li&gt;Extract the model layer into its own package to allow tests to access the database.&lt;/li&gt;
  &lt;li&gt;Use puppeteer via Docker in the test environment for an end-to-end test using a headless browser.&lt;/li&gt;
  &lt;li&gt;Use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jsdom&lt;/code&gt; in the test environment for a lighter weight approach to (mostly) end-to-end testing, blurring the boundary somewhat between frontend integration testing and end-to-end testing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next time, we’ll expand into the world of multiple backend services and explore some strategies for managing and testing them. Until then, happy testing!&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;p&gt;If you’ve read this far, you should &lt;a href=&quot;https://twitter.com/jdleesmiller&quot;&gt;follow me on twitter&lt;/a&gt;, or maybe even apply to work at &lt;a href=&quot;https://www.overleaf.com&quot;&gt;Overleaf&lt;/a&gt;. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;h3 id=&quot;footnotes&quot;&gt;Footnotes&lt;/h3&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:suffix&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;The numeric suffix is so you can run multiple instances of the same container with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--scale&lt;/code&gt; flag to &lt;a href=&quot;https://docs.docker.com/compose/reference/up/&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose up&lt;/code&gt;&lt;/a&gt;. We won’t use this feature here. &lt;a href=&quot;#fnref:suffix&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:project-name&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;One disadvantage of hard coding the project name in this way is that you can’t run two instances of the project in different folders, which the default Compose behavior of using the directory name as the project name allows. Setting it based on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$(basename $(pwd))&lt;/code&gt; would bring this back. &lt;a href=&quot;#fnref:project-name&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:pptr-testing-library&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;There is a package, &lt;a href=&quot;https://github.com/testing-library/pptr-testing-library&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pptr-testing-library&lt;/code&gt;&lt;/a&gt;, that brings some DOM Testing Library-style queries to Puppeteer. At the time of writing, some of the key features around &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;waitForElement&lt;/code&gt; are not yet implemented, but it’s a good start and a promising direction! &lt;a href=&quot;#fnref:pptr-testing-library&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</description>
        <pubDate>Sun, 24 May 2020 12:00:00 +0000</pubDate>
        <link>http://jdlm.info/articles/2020/05/24/testing-node-docker-compose-end-to-end.html</link>
        <guid isPermaLink="true">http://jdlm.info/articles/2020/05/24/testing-node-docker-compose-end-to-end.html</guid>
        
        
        <category>articles</category>
        
      </item>
    
      <item>
        <title>Testing with Node and Docker Compose, Part 2: On the Frontend</title>
        <description>&lt;p&gt;This post is the second in a short series about automated testing in node.js web applications with Docker. Last time, we looked at &lt;a href=&quot;/articles/2019/10/19/testing-node-docker-compose-backend.html&quot;&gt;backend testing&lt;/a&gt;; this time, we’ll look at frontend testing.&lt;/p&gt;

&lt;p&gt;To illustrate, we’ll continue building and testing our example application: a simple TODO list manager. So far, we’ve built and tested a RESTful API to manage the task list:&lt;/p&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Create a task &apos;foo&apos;.&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;curl &lt;span class=&quot;nt&quot;&gt;--header&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Content-Type: application/json&apos;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--data&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;{&quot;description&quot;: &quot;foo&quot;}&apos;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  http://localhost:8080/api/tasks
&lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;task&quot;&lt;/span&gt;:&lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;description&quot;&lt;/span&gt;:&lt;span class=&quot;s2&quot;&gt;&quot;foo&quot;&lt;/span&gt;,&lt;span class=&quot;s2&quot;&gt;&quot;id&quot;&lt;/span&gt;:1&lt;span class=&quot;o&quot;&gt;}}&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# List the tasks, which now include &apos;foo&apos;.&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;curl http://localhost:8080/api/tasks
&lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;tasks&quot;&lt;/span&gt;:[&lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;id&quot;&lt;/span&gt;:1,&lt;span class=&quot;s2&quot;&gt;&quot;description&quot;&lt;/span&gt;:&lt;span class=&quot;s2&quot;&gt;&quot;foo&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;}]}&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Complete task &apos;foo&apos; by its ID.&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;curl &lt;span class=&quot;nt&quot;&gt;-X&lt;/span&gt; DELETE http://localhost:8080/api/tasks/1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In this post, we’ll build and test a frontend that is hopefully more user friendly than &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;curl&lt;/code&gt;! Here’s what it will look like:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;/assets/todo-demo/todo-demo-frontend.gif&quot;&gt;&lt;img src=&quot;/assets/todo-demo/todo-demo-frontend.gif&quot; alt=&quot;Create some todos and complete them&quot; style=&quot;max-width: 448px;&quot; /&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;In particular, this post will cover:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;building a simple frontend with &lt;a href=&quot;https://reactjs.org/&quot;&gt;React&lt;/a&gt;, &lt;a href=&quot;https://getbootstrap.com/&quot;&gt;Bootstrap&lt;/a&gt; and &lt;a href=&quot;https://webpack.js.org/&quot;&gt;webpack&lt;/a&gt;,&lt;/li&gt;
  &lt;li&gt;building the frontend with a multi-stage Dockerfile, for both development and production,&lt;/li&gt;
  &lt;li&gt;running backend and frontend containers in Docker Compose, and&lt;/li&gt;
  &lt;li&gt;frontend testing with &lt;a href=&quot;https://github.com/jsdom/jsdom&quot;&gt;jsdom&lt;/a&gt;, &lt;a href=&quot;https://testing-library.com/docs/react-testing-library/intro&quot;&gt;React Testing Library&lt;/a&gt;, &lt;a href=&quot;http://www.wheresrhys.co.uk/fetch-mock/&quot;&gt;fetch-mock&lt;/a&gt; and &lt;a href=&quot;https://github.com/testdouble/testdouble.js&quot;&gt;testdouble.js&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Subsequent posts will extend the approach developed here to include end-to-end testing and then multiple backend services.&lt;/p&gt;

&lt;p&gt;The code is &lt;a href=&quot;https://github.com/jdleesmiller/todo-demo&quot;&gt;available on GitHub&lt;/a&gt;, and each post in the series has a git tag that marks the &lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/tree/todo-frontend/todo&quot;&gt;corresponding code&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;the-todo-list-manager-frontend&quot;&gt;The TODO List Manager Frontend&lt;/h2&gt;

&lt;p&gt;Let’s start with a tour of the frontend we’ll be developing and testing. The frontend comprises three React components, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;App&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NewTask&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Task&lt;/code&gt;, and a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;taskStore&lt;/code&gt; singleton. Here’s an overview of how they interact:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;/assets/todo-demo/todo-demo-frontend-architecture.svg&quot;&gt;&lt;img src=&quot;/assets/todo-demo/todo-demo-frontend-architecture.svg&quot; alt=&quot;The taskStore talks to the back end. The App component listens to the task store, renders the overall layout, and renders the NewTask and Task components.&quot; /&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;taskStore&lt;/code&gt; is a &lt;a href=&quot;https://en.wikipedia.org/wiki/Singleton_pattern&quot;&gt;singleton&lt;/a&gt; that holds the frontend’s copy of the task list and makes requests to the backend API in response to user actions. (The term ‘store’ comes from redux, but this application doesn’t actually use redux.)&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;App&lt;/code&gt; is a coordinating component. It renders the top level UI, a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NewTask&lt;/code&gt; component and one &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Task&lt;/code&gt; component for each task. It initiates the initial request for tasks when it’s rendered, and it listens for the updated list of tasks from the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;taskStore&lt;/code&gt;. It puts the task list into its &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;state&lt;/code&gt;, so when the task list changes, React will re-render the relevant parts of the UI.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NewTask&lt;/code&gt; component is responsible for creating new tasks, and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Task&lt;/code&gt; component is responsible for displaying a task allowing the user to mark it as completed. Both components call through to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;taskStore&lt;/code&gt; when the user takes an action, which then updates the task list, which triggers a re-render as required.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s what some of the code looks like:&lt;/p&gt;

&lt;h4 id=&quot;frontendsrctask-storejs&quot;&gt;&lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-frontend/todo/frontend/src/task-store.js&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;frontend/src/task-store.js&lt;/code&gt;&lt;/a&gt;&lt;/h4&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;TASKS_API_ROOT&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;/api/tasks&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;TaskStore&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;constructor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;tasks&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;listener&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;listen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;listener&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;listener&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;listener&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;unlisten&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;listener&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;list&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fetchJson&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;rootUrl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ok&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Failed to list tasks&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;body&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;tasks&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;tasks&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;listener&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fetchJson&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;rootUrl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;POST&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;description&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ok&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;400&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;body&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Failed to create task&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;list&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;complete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fetchJson&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;itemUrl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;DELETE&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ok&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`Failed to complete &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// ignore&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;list&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fetchJson&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;Accept&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;application/json&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Content-Type&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;application/json&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;options&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;rootUrl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;URL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;TASKS_API_ROOT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;window&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;location&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;origin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;itemUrl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`bad id: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;URL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;rootUrl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;taskStore&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;TaskStore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;taskStore&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;A few notable points:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;taskStore&lt;/code&gt; singleton implements a very simple event system: one listener can call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;taskStore.listen&lt;/code&gt; with a callback to be notified when the task list changes. In this case, there’s only one listener, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;App&lt;/code&gt; component, so this is sufficient, but of course a more general event system could be used if needed. Using events here helps reduce coupling between the store and the rest of the frontend; in particular, the logic in the store doesn’t depend on React.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;taskStore&lt;/code&gt; could try to be smart and update its local &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tasks&lt;/code&gt; array in response to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;create&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;complete&lt;/code&gt; calls, but for now it just re-requests the whole task list from the backend whenever it knows the list has changed. This keeps the store simpler.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;It uses &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fetch&lt;/code&gt;&lt;/a&gt; to talk to the backend through a small wrapper function, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fetchJson&lt;/code&gt;, which sets up the required headers. And it uses &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/URL&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;URL&lt;/code&gt;&lt;/a&gt; to build the API URLs, to slightly reduce the amount of manual URL string munging required.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next let’s look at the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;App&lt;/code&gt; component, which uses the task list from the store:&lt;/p&gt;

&lt;h4 id=&quot;frontendsrccomponentappjs&quot;&gt;&lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-frontend/todo/frontend/src/component/app.js&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;frontend/src/component/app.js&lt;/code&gt;&lt;/a&gt;&lt;/h4&gt;

&lt;div class=&quot;language-jsx highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;React&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;react&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;NewTask&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;./new-task&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Task&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;./task&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;taskStore&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;../task-store&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;App&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;React&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Component&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;constructor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;props&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;super&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;props&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;tasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;listError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;componentDidMount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;taskStore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;listen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;setState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;tasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;taskStore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;tasks&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}))&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;_listTasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;componentWillUnmount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;taskStore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;unlisten&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;_listTasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;taskStore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;list&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;listError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;setState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;listError&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;listItems&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;listError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;listItems&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;li&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;className&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;list-group-item&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
          Failed to load tasks. Please refresh the page.
        &lt;span class=&quot;p&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;li&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;tasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;listItems&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;tasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(({&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;description&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
          &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Task&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;listItems&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;li&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;className&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;list-group-item&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;Loading&lt;span class=&quot;ni&quot;&gt;&amp;amp;hellip;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;li&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;className&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;container&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;className&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;row&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;className&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;col&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;h1&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;className&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;mt-5 mb-3 text-center&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;TO DO&lt;span class=&quot;p&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;h1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;p&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;div&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;div&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;className&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;row&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;className&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;col&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;ul&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;className&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;list-group&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
              &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;NewTask&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;/&amp;gt;&lt;/span&gt;
              &lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;listItems&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;ul&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;p&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;div&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;div&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;div&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;It looks fairly long, but most of it is just error handling, latency compensation and some rather verbose HTML for bootstrap styles.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;The component starts off in a ‘Loading…’ state with no tasks.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;It asks the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;taskStore&lt;/code&gt; to start the first data fetch in the background and notify it of the results, using React’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;componentDidMount&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;componentWillUnmount&lt;/code&gt; lifecycle methods.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Once the data comes back, it will render the list, or, if an error occurred display a (not particularly great) error message.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The child components handle most of the UI. Let’s look at the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Task&lt;/code&gt; component:&lt;/p&gt;

&lt;h4 id=&quot;frontendsrccomponenttaskjs&quot;&gt;&lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-frontend/todo/frontend/src/component/task.js&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;frontend/src/component/task.js&lt;/code&gt;&lt;/a&gt;&lt;/h4&gt;

&lt;div class=&quot;language-jsx highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;PropTypes&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;prop-types&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;React&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useEffect&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;react&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;taskStore&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;../task-store&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Task&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;description&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;completing&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;setCompleting&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;useEffect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;unmounted&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;complete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;taskStore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;complete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;err&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;alert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;err&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;finally&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;unmounted&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;setCompleting&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;completing&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;complete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;unmounted&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;completing&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;li&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;className&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;list-group-item todo-task&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;form&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;onSubmit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;e&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;nx&quot;&gt;setCompleting&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
          &lt;span class=&quot;nx&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;preventDefault&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;span&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;span&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;button&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;className&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;btn btn-success float-right&quot;&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;submit&quot;&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;style&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;minWidth&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;3em&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;disabled&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;completing&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;aria-label&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`mark &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; complete`&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;
          &lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
            ✓
          &lt;span class=&quot;p&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;button&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;li&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;Task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;propTypes&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;PropTypes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;PropTypes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;string&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Task&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This is a smaller component, but again there are a few things to comment on:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;Since the component is mostly driven by its &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;props&lt;/code&gt; and has relatively little state, I’ve written it as a functional component that uses &lt;a href=&quot;https://reactjs.org/docs/hooks-intro.html&quot;&gt;React hooks&lt;/a&gt;. It has one state variable, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;completing&lt;/code&gt;, that tracks whether there is currently a request in progress and disables the button.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Because the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;taskStore&lt;/code&gt; updates the list asynchronously, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;taskStore.complete&lt;/code&gt; call can finish after the component has been unmounted, in which case trying to update the state is an error. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unmounted&lt;/code&gt; variable tracks when the component is unmounted, to avoid this.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The button sets an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aria-label&lt;/code&gt; to be friendlier to screen readers, for improved accessibility. We’ll also see that it’s helpful in testing, later.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-frontend/todo/frontend/src/component/new-task.js&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NewTask&lt;/code&gt; component&lt;/a&gt; is similar.&lt;/p&gt;

&lt;p&gt;Finally, to bring it together, we also need &lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-frontend/todo/frontend/src/index.html&quot;&gt;an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index.html&lt;/code&gt; file&lt;/a&gt; and &lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-frontend/todo/frontend/src/index.js&quot;&gt;an entrypoint&lt;/a&gt; to load up all our various polyfills and render the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;App&lt;/code&gt; component.&lt;/p&gt;

&lt;h2 id=&quot;packaging-backend-and-frontend&quot;&gt;Packaging Backend and Frontend&lt;/h2&gt;

&lt;p&gt;Now that we have the frontend code, let’s see how to build it and get it working with the backend express application that we developed in &lt;a href=&quot;/articles/2019/10/19/testing-node-docker-compose-backend.html&quot;&gt;part 1&lt;/a&gt;. The main points are:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;Put the backend and the frontend into separate packages. Each can then have its own &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;package.json&lt;/code&gt; file and dependencies (and dev dependencies). Frontend applications these days have a &lt;em&gt;lot&lt;/em&gt; of dependencies that are not needed on the backend. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm audit&lt;/code&gt; reports that our todo demo app frontend pulls in 10,997 packages, most of which are related to webpack.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;In development, run the backend and the frontend in separate containers. The backend container runs the backend service as usual, and the frontend container, which is what our browser will talk to, runs an instance of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;webpack-dev-server&lt;/code&gt; configured to serve the frontend and proxy the API endpoints through to the backend container.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;For production, build the frontend using a multistage &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt; shared between the backend and the frontend. This lets us build the frontend in one stage with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;webpack&lt;/code&gt; and then copy the resulting build artifacts from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dist&lt;/code&gt; into the production backend image in a later stage, so the backend can simply serve them as static files.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This may be clearer in diagram form:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;/assets/todo-demo/todo-demo-frontend-approach.svg&quot;&gt;&lt;img src=&quot;/assets/todo-demo/todo-demo-frontend-approach.svg&quot; alt=&quot;Requests in development go through webpack dev server, which serves frontend requests itself and proxies through to the backend for API requests. In production there is no webpack dev server; the backend serves the frontend files and handles API requests directly.&quot; style=&quot;max-width: 448px;&quot; /&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;Let’s start with the multistage &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt; responsible for producing the images for both the backend and the frontend. It uses the techniques that I covered in the &lt;a href=&quot;/articles/2019/09/06/lessons-building-node-app-docker.html#docker-for-dev-and-prod&quot;&gt;first Docker post&lt;/a&gt; to avoid running as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;root&lt;/code&gt;, and to the use of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;slim&lt;/code&gt; images for smaller image sizes, so I’ll skip the details related to those things and talk about the overall structure below.&lt;/p&gt;

&lt;h4 id=&quot;dockerfile&quot;&gt;&lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-frontend/todo/Dockerfile&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt;&lt;/a&gt;&lt;/h4&gt;

&lt;div class=&quot;language-docker highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;#&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Backend for Development&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;#&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;node:12&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;development-backend&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;RUN &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; /srv/todo/backend &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;chown&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-R&lt;/span&gt; node:node /srv/todo

&lt;span class=&quot;k&quot;&gt;USER&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; node&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;WORKDIR&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; /srv/todo/backend&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;COPY&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; --chown=node:node backend/package.json backend/package-lock.json ./&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;RUN &lt;/span&gt;npm &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--quiet&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;#&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Frontend for Development&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;#&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;node:12&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;development-frontend&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;RUN &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; /srv/todo/frontend/dist &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;chown&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-R&lt;/span&gt; node:node /srv/todo

&lt;span class=&quot;k&quot;&gt;USER&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; node&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;WORKDIR&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; /srv/todo/frontend&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;COPY&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; frontend/package.json frontend/package-lock.json ./&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;RUN &lt;/span&gt;npm &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--quiet&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;#&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Frontend Build for Production&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;#&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;development-frontend&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;build-frontend&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;COPY&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; frontend .&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;RUN &lt;/span&gt;npm run build

&lt;span class=&quot;c&quot;&gt;#&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Backend for Production&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;#&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;node:12-slim&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;production&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;USER&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; node&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;WORKDIR&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; /srv/todo/backend&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;COPY&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; --from=development-backend --chown=root:root /srv/todo/backend/node_modules ./node_modules&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;COPY&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; --from=build-frontend --chown=root:root /srv/todo/frontend/dist ./dist&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;COPY&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; . .&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;CMD&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; [&quot;node&quot;, &quot;server.js&quot;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The stages are laid out linearly in the file, but it may be easier to follow in this graph, which shows what each stage derives or copies files from:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;/assets/todo-demo/multistage-dockerfile.svg&quot;&gt;&lt;img src=&quot;/assets/todo-demo/multistage-dockerfile.svg&quot; alt=&quot;development-backend, development-frontend and build-frontend all derive from node:12; production derives from node:12-slim and copies in node_modules from development-backend and dist from build-frontend&quot; style=&quot;max-width: 448px;&quot; /&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;development-backend&lt;/code&gt; stage installs the dependencies for the backend &lt;sup id=&quot;fnref:dev-dependencies&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:dev-dependencies&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;. It doesn’t actually copy the application source into the image, because we’ll instead bind mount those files into the container with Docker Compose, below.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;development-frontend&lt;/code&gt; stage does the same for the frontend.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;build-frontend&lt;/code&gt; stage runs the webpack build. It starts from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;development-frontend&lt;/code&gt; stage rather than a node base image, so it can use the packages we just installed, and it copies in the frontend source files, which are the inputs for the webpack build.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;production&lt;/code&gt; stage copies in the dependencies from the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;development-backend&lt;/code&gt; so it can run the backend express application, and it copies the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dist&lt;/code&gt; folder, which is the output of the webpack build, so it can serve the frontend &lt;sup id=&quot;fnref:cdn&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:cdn&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In development, we won’t actually build the production stage, so it’s just the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;development-backend&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;development-frontend&lt;/code&gt; that we’ll need in this post. Those will be referenced in the docker-compose file, which will change from &lt;a href=&quot;/articles/2019/10/19/testing-node-docker-compose-backend.html#docker-composeyml&quot;&gt;part 1&lt;/a&gt; to the following, with an additional &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;frontend&lt;/code&gt; service:&lt;/p&gt;

&lt;h4 id=&quot;docker-composeyml&quot;&gt;&lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-frontend/todo/docker-compose.yml&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose.yml&lt;/code&gt;&lt;/a&gt;&lt;/h4&gt;

&lt;div class=&quot;language-yml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;3.7&apos;&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;backend&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;.&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;development-backend&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;npx nodemon server.js&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;depends_on&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;postgres&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;PORT&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;8080&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;./backend:/srv/todo/backend&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;backend_node_modules:/srv/todo/backend/node_modules&lt;/span&gt;

  &lt;span class=&quot;na&quot;&gt;frontend&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;.&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;development-frontend&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;npx webpack-dev-server&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;depends_on&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;backend&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;HOST&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;frontend&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;PORT&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;8080&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;8080:8080&apos;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;./frontend:/srv/todo/frontend&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;frontend_node_modules:/srv/todo/frontend/node_modules&lt;/span&gt;

  &lt;span class=&quot;na&quot;&gt;postgres&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;postgres:12&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;POSTGRES_HOST_AUTH_METHOD&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;trust&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;backend_node_modules&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;frontend_node_modules&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Key points are:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;The name of the Dockerfile stage targeted by the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;backend&lt;/code&gt; service changed from just &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;development&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;development-backend&lt;/code&gt;, as discussed above.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;frontend&lt;/code&gt; service runs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;webpack-dev-server&lt;/code&gt;. It depends on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;backend&lt;/code&gt; service so it can proxy requests for the API back to the backend.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The exposed port &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;8080&lt;/code&gt; moved from the backend service to the frontend service, so we can view the frontend in the browser.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node_modules&lt;/code&gt; volume trick described in the &lt;a href=&quot;/articles/2019/09/06/lessons-building-node-app-docker.html#the-node_modules-volume-trick&quot;&gt;first Docker post&lt;/a&gt; is repeated for the frontend, so there are now two volumes that contain the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node_modules&lt;/code&gt;. I have also changed the paths to reflect the separation into &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;backend&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;frontend&lt;/code&gt; packages.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Finally, we need the to set up &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;webpack.config.js&lt;/code&gt; to do the proxying. Here’s the relevant part of the webpack config that proxies requests from the browser to the frontend under &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/api&lt;/code&gt; through to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;backend&lt;/code&gt; service:&lt;/p&gt;

&lt;h4 id=&quot;frontendwebpackconfigjs&quot;&gt;&lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-frontend/todo/frontend/webpack.config.js&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;frontend/webpack.config.js&lt;/code&gt;&lt;/a&gt;&lt;/h4&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// ... setup ...&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;module&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;exports&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;mode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;process&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;NODE_ENV&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;production&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;production&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;development&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;devServer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Allow connections from outside the container (not much use otherwise).&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;host&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;process&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;HOST&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;0.0.0.0&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Proxy api routes through to the todo backend.&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;proxy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;/api&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;`http://backend:&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;process&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;PORT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;entry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;./src/index.js&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// ... more config ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;With all that in place, we can now bring up the frontend and backend with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/up&lt;/code&gt; script from &lt;a href=&quot;/articles/2019/10/19/testing-node-docker-compose-backend.html#binup&quot;&gt;part 1&lt;/a&gt;, and see the UI on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;http://localhost:8080&lt;/code&gt;. Here’s that gif again:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;/assets/todo-demo/todo-demo-frontend.gif&quot;&gt;&lt;img src=&quot;/assets/todo-demo/todo-demo-frontend.gif&quot; alt=&quot;Create some todos and complete them&quot; style=&quot;max-width: 448px;&quot; /&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;h2 id=&quot;the-tests&quot;&gt;The Tests&lt;/h2&gt;

&lt;p&gt;Now that we have a frontend, let’s look at how to test it. I’ve written three types of tests, &lt;em&gt;model tests&lt;/em&gt;, &lt;em&gt;component tests&lt;/em&gt;, and &lt;em&gt;integration tests&lt;/em&gt;. Like on the &lt;a href=&quot;/articles/2019/10/19/testing-node-docker-compose-backend.html#the-tests&quot;&gt;backend&lt;/a&gt;, these types of tests can be understood in terms of the different layers of the &lt;a href=&quot;https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller&quot;&gt;Model-View-Controller&lt;/a&gt; (MVC) architecture.&lt;/p&gt;

&lt;p&gt;The model tests test the model layer, which contains the application’s core business logic. In this case, the entire model layer for the frontend is essentially the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TaskStore&lt;/code&gt; class.&lt;/p&gt;

&lt;p&gt;The component tests test the controller and view layers. In React applications, the controller and view layers tend to be combined together with in the same component. For example, the &lt;a href=&quot;#frontendsrccomponenttaskjs&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Task&lt;/code&gt;&lt;/a&gt; component has logic for dealing with in progress operations and error handling, as well as the JSX code that actually generates the DOM for the UI. So, at least for a React application, it makes sense to test these two layers together, component-by-component.&lt;/p&gt;

&lt;p&gt;Finally, the integration tests test that the models and components work together as expected. In diagram form:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;/assets/todo-demo/todo-frontend-tests.svg&quot;&gt;&lt;img src=&quot;/assets/todo-demo/todo-frontend-tests.svg&quot; alt=&quot;The frontend comprises models, which are tested with model tests, and components, which are tested with component tests. Integration tests test both models and components. The backend is not included in any of the frontend tests.&quot; style=&quot;max-width: 448px;&quot; /&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;Notably, the frontend tests don’t include the backend. I have instead faked the HTTP requests to the backend API for frontend testing; more on this in the conclusion.&lt;/p&gt;

&lt;h3 id=&quot;integration-tests&quot;&gt;Integration Tests&lt;/h3&gt;

&lt;p&gt;Let’s start with the integration tests. In this case, there’s only one, which is essentially a ‘happy path’ test for the whole frontend — listing, creating and completing tasks.&lt;/p&gt;

&lt;h4 id=&quot;frontendtestintegrationtodotestjs&quot;&gt;&lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-frontend/todo/frontend/test/integration/todo.test.js&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;frontend/test/integration/todo.test.js&lt;/code&gt;&lt;/a&gt;&lt;/h4&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;React&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;react&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;cleanup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;fireEvent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;waitForElement&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;waitForElementToBeRemoved&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;@testing-library/react&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fetchMock&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;../support/fetch-mock&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;App&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;../../src/component/app&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;describe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;TO DO App&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;afterEach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;cleanup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;afterEach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fetchMock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;reset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;lists, creates and completes tasks&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Load empty list.&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;fetchMock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getOnce&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;path:/api/tasks&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;tasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;getByText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;getByLabelText&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;App&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;description&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;getByLabelText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;new task description&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;addTask&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;getByLabelText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;add task&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;waitForElementToBeRemoved&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;getByText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/loading/i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// Create &apos;find keys&apos; task.&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;fetchMock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;postOnce&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;path:/api/tasks&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;find keys&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;fetchMock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getOnce&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;path:/api/tasks&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;tasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;find keys&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}]&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;fireEvent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;change&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;find keys&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;fireEvent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;click&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;addTask&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;waitForElement&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;getByText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;find keys&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// Create &apos;buy milk&apos; task.&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;fetchMock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;postOnce&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;path:/api/tasks&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;buy milk&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;fetchMock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getOnce&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;path:/api/tasks&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;tasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;find keys&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;buy milk&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;fireEvent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;change&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;buy milk&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;fireEvent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;click&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;addTask&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;waitForElement&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;getByText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;buy milk&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// Complete &apos;buy milk&apos; task.&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;fetchMock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;deleteOnce&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;path:/api/tasks/2&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;204&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;fetchMock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getOnce&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;path:/api/tasks&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;tasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;find keys&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}]&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;

    &lt;span class=&quot;nx&quot;&gt;fireEvent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;click&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getByLabelText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;mark buy milk complete&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;waitForElementToBeRemoved&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;getByText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;buy milk&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// Complete &apos;find keys&apos; task.&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;fetchMock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;deleteOnce&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;path:/api/tasks/1&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;204&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;fetchMock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getOnce&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;path:/api/tasks&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;tasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;

    &lt;span class=&quot;nx&quot;&gt;fireEvent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;click&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getByLabelText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;mark find keys complete&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;waitForElementToBeRemoved&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;getByText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;find keys&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;The test uses &lt;a href=&quot;https://testing-library.com/docs/react-testing-library/intro&quot;&gt;React Testing Library&lt;/a&gt;, which provides test helpers that simulate a user interacting with the React application. The library’s motto is “The more your tests resemble the way your software is used, the more confidence they can give you.”, which I generally agree with.&lt;/p&gt;

    &lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;getByText&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;getByLabelText&lt;/code&gt; functions returned by React Testing Library’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;render&lt;/code&gt; function let us query for UI elements using text visible to the user or, in some cases, screen readers via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aria&lt;/code&gt; properties. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fireEvent&lt;/code&gt; function lets us then click on or type in those elements. Finally, the asynchronous &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;waitForElement&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;waitForElementToBeRemoved&lt;/code&gt; functions let the test wait for an expected change to the UI.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The test starts by rendering the top level &lt;a href=&quot;#frontendsrccomponentappjs&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;App&lt;/code&gt;&lt;/a&gt; component, which causes the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TaskStore&lt;/code&gt; to request the task list from the API. The test uses &lt;a href=&quot;https://www.npmjs.com/package/fetch-mock&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fetchMock&lt;/code&gt;&lt;/a&gt; before rendering the component to set up a fake response that returns an empty task list when the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TaskStore&lt;/code&gt; requests it. The test then largely repeats in this pattern — mock the requests we expect, manipulate the UI, and then wait for it to reach the expected state.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;model-tests&quot;&gt;Model Tests&lt;/h3&gt;

&lt;p&gt;As mentioned, this application’s model layer is essentially all in one class, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TaskStore&lt;/code&gt;. Since the integration tests exercise the happy path well, including getting the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TaskStore&lt;/code&gt; to make requests and verifying them with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fetchMock&lt;/code&gt;, the model tests can focus mainly on error handling.&lt;/p&gt;

&lt;h4 id=&quot;frontendtestmodeltask-storetestjs&quot;&gt;&lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-frontend/todo/frontend/test/model/task-store.test.js&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;frontend/test/model/task-store.test.js&lt;/code&gt;&lt;/a&gt;&lt;/h4&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;assert&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;td&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;testdouble&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fetchMock&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;../support/fetch-mock&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;taskStore&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;../../src/task-store&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;describe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;TaskStore&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;afterEach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fetchMock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;reset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;afterEach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;taskStore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;unlisten&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;lists tasks&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;listener&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;td&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;taskStore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;listen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;listener&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;nx&quot;&gt;fetchMock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getOnce&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;path:/api/tasks&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;tasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;foo&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;bar&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}]&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;taskStore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;list&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;assert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;taskStore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;tasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;length&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;nx&quot;&gt;td&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;verify&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;listener&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;handles 500 error on listing&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;taskStore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;listen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;shouldNotCallListener&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;fetchMock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getOnce&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;path:/api/tasks&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;500&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;taskStore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;list&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;err&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;assert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;strictEqual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;err&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Failed to list tasks&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;handles 400 error on task creation&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;taskStore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;listen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;shouldNotCallListener&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;fetchMock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;postOnce&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;path:/api/tasks&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;400&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;test: create failed&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;taskStore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;err&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;assert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;strictEqual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;err&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;test: create failed&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;handles 500 error on task creation&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;taskStore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;listen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;shouldNotCallListener&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;fetchMock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;postOnce&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;path:/api/tasks&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;500&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;taskStore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;find keys&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;err&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;assert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;strictEqual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;err&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Failed to create task&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;handles 500 error on task completion&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;taskStore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;listen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;shouldNotCallListener&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;fetchMock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;deleteOnce&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;path:/api/tasks/1&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;500&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;taskStore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;complete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;err&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;assert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;strictEqual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;err&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Failed to complete 1&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;shouldNotCallListener&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;should not call listener&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;Like the integration tests, these tests use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fetchMock&lt;/code&gt; to mock the HTTP requests that the store makes. They also use (one small part of) &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;testdouble&lt;/code&gt; to verify that the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TaskStore&lt;/code&gt;’s listener is called at the expected times, namely when the UI should be updated with the new task list.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The first model test is a ‘sanity test’ that overlaps a bit with the integration tests, in that it’s a ‘happy path’ test. We could write similar happy path model tests for the other public methods of the task store, but it would not be add much value in this case — the happy path for the single consumer of this &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TaskStore&lt;/code&gt; is relatively simple and well covered &lt;sup id=&quot;fnref:coverage&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:coverage&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;3&lt;/a&gt;&lt;/sup&gt; by an integration test, so testing it exhaustively at model level would yield diminishing returns. If the ‘happy path’ were more complex, for example with more branching into many ‘happy paths’, it might make sense to only test some of the happy paths at integration test level and test the rest at model level. And if there were many consumers for this &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TaskStore&lt;/code&gt;, having more complete model testing might help to clarify the &lt;a href=&quot;https://en.wikipedia.org/wiki/Design_by_contract&quot;&gt;contract&lt;/a&gt; that it has with those consumers.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The remaining tests are for error conditions. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fetchMock&lt;/code&gt; makes it easy to simulate errors from the backend, such as status 400 or 500 responses — a good use case for mocking.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;component-tests&quot;&gt;Component Tests&lt;/h3&gt;

&lt;p&gt;Finally, we have the component tests. The ‘happy path’ integration test did exercise the components, but it didn’t make any assertions about latency compensation or error handling, which is where most of the complexity in the components comes from. So, these areas are a good focus for the component tests. Let’s look at the tests for the &lt;a href=&quot;#frontendsrccomponenttaskjs&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Task&lt;/code&gt;&lt;/a&gt; component:&lt;/p&gt;

&lt;h4 id=&quot;frontendtestcomponenttasktestjs&quot;&gt;&lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/master/todo/frontend/test/component/task.test.js&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;frontend/test/component/task.test.js&lt;/code&gt;&lt;/a&gt;&lt;/h4&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;React&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;react&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;td&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;testdouble&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;cleanup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fireEvent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;wait&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;@testing-library/react&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Task&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;../../src/component/task&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;taskStore&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;../../src/task-store&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;describe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Task&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;afterEach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;cleanup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;afterEach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;td&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;reset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;testId&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;123&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;testDescription&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;find keys&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;complete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;completeResolve&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;completeReject&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;beforeEach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;getByText&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Task&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;testId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;testDescription&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;sr&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&amp;gt;
&lt;/span&gt;    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;complete&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;getByText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;✓&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;taskStoreComplete&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;td&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;taskStore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;complete&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;completePromise&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;reject&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;completeResolve&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;resolve&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;completeReject&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;reject&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;td&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;when&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;taskStoreComplete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;testId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;thenReturn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;completePromise&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;nx&quot;&gt;fireEvent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;click&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;complete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;completes a task&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// The button should be disabled while we&apos;re submitting.&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;wait&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;complete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;disabled&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;completeResolve&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;wait&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;complete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;disabled&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;handles failure to complete&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;alert&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;td&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;global&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;alert&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// yes, I&apos;m using alert&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// The button should be disabled while we&apos;re submitting.&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;wait&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;complete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;disabled&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;completeReject&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;test message&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;wait&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;complete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;disabled&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// Show the user the error message (ideally would be friendlier).&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;td&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;verify&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;alert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;test message&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;The component tests use React Testing Library to render the component under test and make assertions about it, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;testdouble&lt;/code&gt; to mock the interactions with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TaskStore&lt;/code&gt;. We could instead include the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TaskStore&lt;/code&gt; in the system under test and mock the requests it makes with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fetch-mock&lt;/code&gt;, but since the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;taskStore&lt;/code&gt; singleton presents a relatively simple interface for mocking, and we’d have to mock somewhere, I’ve gone with mocking at the model level here. (See also &lt;a href=&quot;/articles/2019/10/19/testing-node-docker-compose-backend.html#appendix-views-on-testing&quot;&gt;part 1&lt;/a&gt; for a discussion of when and what to mock.)&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Both tests here relate to what happens when the user clicks the complete button for the task, so the code to set up the state for the test is in a shared &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;beforeEach&lt;/code&gt;. If we had a wider range of states and behaviors to set up, some of it would probably be better moved to a separate &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;describe&lt;/code&gt; with its own &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;beforeEach&lt;/code&gt;, but here I’ve just put it all together to keep it simple (YAGNI).&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The tests check that the button is disabled (to prevent double clicks) and re-enabled in both the success and error cases, and that an error message is shown in the error case.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-frontend/todo/frontend/test/component/new-task.test.js&quot;&gt;tests for the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NewTask&lt;/code&gt; component&lt;/a&gt; and the &lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-frontend/todo/frontend/test/component/app.test.js&quot;&gt;App component&lt;/a&gt; are similar.&lt;/p&gt;

&lt;p&gt;One thing that these component tests don’t cover is how things look, or indeed anything about the structure of the DOM they generate, other than the presence of appropriate buttons or text. Assertion-based testing like that above tends to be quite time-consuming when used at that level of detail, both initially to create and over time to update when anything changes. And, in my experience, subtle bugs still tend to slip through, for example due to CSS making things invisible or unclickable. So, here I’ve left the visual testing to manual testing. Another approach that seems interesting is snapshot testing, in which we just record what was rendered so that any changes can be noted, but I have not so far used it in anger. Some of the visual testing can also be achieved with end-to-end tests, which will be the subject of the next post in this series.&lt;/p&gt;

&lt;h3 id=&quot;running-the-tests&quot;&gt;Running the Tests&lt;/h3&gt;

&lt;p&gt;So, we have finally reached the part where we can run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm test&lt;/code&gt; in a container. In particular, the tests will run in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;frontend&lt;/code&gt; container. They are set up to run in node with &lt;a href=&quot;https://github.com/jsdom/jsdom&quot;&gt;jsdom&lt;/a&gt; and &lt;a href=&quot;https://babeljs.io/&quot;&gt;babel&lt;/a&gt;, which requires some &lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-frontend/todo/frontend/test/setup.js&quot;&gt;setup&lt;/a&gt; when running &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mocha&lt;/code&gt;. This allows us to run the tests on the command line in a simulated browser environment, which is quicker to set up than a full browser.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ docker-compose run --rm frontend npm test
Starting todo_postgres_1 ... done
Starting todo_backend_1     ... done

&amp;gt; frontend@1.0.0 test /srv/todo/frontend
&amp;gt; mocha --require test/setup



  App
    ✓ handles a failure to list tasks (587ms)

  NewTask
    ✓ creates a new task
    ✓ handles failure to create

  Task
    ✓ completes a task
    ✓ handles failure to complete

  TO DO App
    ✓ lists, creates and completes tasks (198ms)

  TaskStore
    ✓ lists tasks
    ✓ handles 500 error on listing (49ms)
    ✓ handles 400 error on task creation
    ✓ handles 500 error on task creation
    ✓ handles 500 error on task completion


  11 passing (1s)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That said, it’s nice to also be able to run the tests in a real browser, and there is a handy webpack loader, &lt;a href=&quot;https://www.npmjs.com/package/mocha-loader&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mocha-loader&lt;/code&gt;&lt;/a&gt; that can handle this. I added a short script to get it to expose the test runner on another port using Docker Compose’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--publish&lt;/code&gt; flag:&lt;/p&gt;

&lt;h4 id=&quot;binbrowser-test&quot;&gt;&lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-frontend/todo/bin/browser-test&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/browser-test&lt;/code&gt;&lt;/a&gt;&lt;/h4&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;#!/usr/bin/env bash&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;#&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Run the frontend tests in a browser on http://localhost:8181 .&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;#&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-e&lt;/span&gt;

docker-compose run &lt;span class=&quot;nt&quot;&gt;--rm&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--publish&lt;/span&gt; 8181:8181 &lt;span class=&quot;nt&quot;&gt;--use-aliases&lt;/span&gt; frontend &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  npx webpack-dev-server &lt;span class=&quot;s1&quot;&gt;&apos;mocha-loader!./test/index.js&apos;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--port&lt;/span&gt; 8181 &lt;span class=&quot;nt&quot;&gt;--hot&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--inline&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--output-filename&lt;/span&gt; test.js
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;One caveat is that we need to provide an entrypoint, &lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/master/todo/frontend/test/index.js&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;test/index.js&lt;/code&gt;&lt;/a&gt;, that imports all the other tests, rather than letting mocha find them on disk itself. This is not so bad, and it’s still possible to run individual tests with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--grep&lt;/code&gt;. Here’s what it looks like in the browser:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;/assets/todo-demo/browser-test.png&quot;&gt;&lt;img src=&quot;/assets/todo-demo/browser-test.png&quot; alt=&quot;Each group of tests has a heading, and within it each test is listed with a green tick mark.&quot; style=&quot;max-width: 448px;&quot; /&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;h1 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h1&gt;

&lt;p&gt;We’ve built, packaged and tested a frontend application using webpack and React, and we’ve wired it up to our backend using Docker and Docker Compose. In particular, we’ve seen that putting the backend and the frontend into the same multistage &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt; allows Docker to copy the frontend build artifacts to the backend so the backend can serve them, and that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;webpack-dev-server&lt;/code&gt;’s proxy feature is great for exposing the backend API to the frontend.&lt;/p&gt;

&lt;p&gt;We’ve seen three kinds of frontend tests that, like the tests on the backend, focus on different layers of the model-view-controller architecture. In particular, model tests test the frontend model layer, mocking out the HTTP requests to the backend. Component tests test the view and controller layers, mocking out the model layer. And a ‘happy path’ integration test checks that the models and components work together, again mocking the backend requests and responses.&lt;/p&gt;

&lt;p&gt;This highlights a potential problem with our tests: we could change the way the backend works in such a way that the frontend would break, and the tests would all still pass, because the mocked requests in the frontend tests would be out of date. Next time, we’ll set up some end-to-end tests that will test the backend and frontend together. See you then!&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/articles/2020/05/24/testing-node-docker-compose-end-to-end.html&quot;&gt;Onward to part 3!&lt;/a&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;p&gt;If you’ve read this far, you should &lt;a href=&quot;https://twitter.com/jdleesmiller&quot;&gt;follow me on twitter&lt;/a&gt;, or maybe even apply to work at &lt;a href=&quot;https://www.overleaf.com&quot;&gt;Overleaf&lt;/a&gt;. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;h1 id=&quot;footnotes&quot;&gt;Footnotes&lt;/h1&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:dev-dependencies&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;This Dockerfile copies the backend dev dependencies (in addition to the runtime dependencies) into the production image. If you would rather not do this, it can be avoided by adding an additional &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;build-backend&lt;/code&gt; step that installs only the production dependencies. However, because the Dockerfile is already quite long, I’ll leave this as an exercise for the reader. &lt;a href=&quot;#fnref:dev-dependencies&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:cdn&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;This approach of bundling the single latest version of the frontend in the production is very clean and container-y and works well at small scale. However, assuming webpack is set up to use asset hashing, you can easily get into a situation where changes to the frontend will result in 404s on assets during or soon after a deploy, because clients are asking for old versions of assets that aren’t in the new image. There are a few ways to fix this:&lt;/p&gt;

      &lt;ul&gt;
        &lt;li&gt;If using blue-green deployments, putting a pull-mode &lt;a href=&quot;https://en.wikipedia.org/wiki/Content_delivery_network#Content_networking_techniques&quot;&gt;CDN&lt;/a&gt; or caching reverse proxy, such as CloudFlare, in front of the application should in most cases keep old versions of the frontend assets around for long enough. Another option is to add a build cache to the image build process to copy some older versions of the frontend assets in, as well as the latest version. This is similar to how Heroku’s build packs and build cache work.&lt;/li&gt;
        &lt;li&gt;If using rolling deployments, which are popular in Kubernetes environments, a push-mode CDN is required, because otherwise requests for new assets might land on pods that are still running the old code, resulting in 404s on the new assets instead of the old ones. The approach here would be to extract the assets from the image before it’s deployed, e.g. with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker cp&lt;/code&gt; from a temporary container, and push them up to the CDN. Or you could do a two-phase deploy, with the old backend plus new assets and then the new backend plus new assets.&lt;/li&gt;
      &lt;/ul&gt;

      &lt;p&gt;It’s never easy. &lt;a href=&quot;#fnref:cdn&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:coverage&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;This article is long enough without also talking about measuring code coverage, but I think it is a good thing to do. Here it’s simple enough to see which tests cover which code, but in a larger application it is often less clear. Code coverage is particularly helpful when the time comes to optimize the test suite. The process of identifying redundant tests or those that add little coverage relative to their running times can then be quantified and to some extent automated. It can also help identify dead code to be deleted. &lt;a href=&quot;#fnref:coverage&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</description>
        <pubDate>Sun, 12 Jan 2020 20:00:00 +0000</pubDate>
        <link>http://jdlm.info/articles/2020/01/12/testing-node-docker-compose-frontend.html</link>
        <guid isPermaLink="true">http://jdlm.info/articles/2020/01/12/testing-node-docker-compose-frontend.html</guid>
        
        
        <category>articles</category>
        
      </item>
    
      <item>
        <title>Testing with Node and Docker Compose, Part 1: On the Backend</title>
        <description>&lt;p&gt;My &lt;a href=&quot;/articles/2019/09/06/lessons-building-node-app-docker.html&quot;&gt;last post&lt;/a&gt; covered the basics of how to get a node.js application running in Docker. This post is the first in a short series about automated testing in node.js web applications with Docker.&lt;/p&gt;

&lt;p&gt;It boils down to running &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm test&lt;/code&gt; in a Docker container, which may not seem like it should require multiple blog posts! However, as an application gets more complicated and requires more kinds of testing, such as frontend testing and end-to-end testing across multiple services, getting to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm test&lt;/code&gt; can be nontrivial. Fortunately, Docker and Docker Compose provide tools that can help.&lt;/p&gt;

&lt;p&gt;To illustrate, we’ll build and test an example application: a small TODO list manager. Here’s what it will look like when finished, at the end of the series:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;/assets/todo-demo/todo-demo.gif&quot;&gt;&lt;img src=&quot;/assets/todo-demo/todo-demo.gif&quot; alt=&quot;Create some todos, search, and complete them&quot; style=&quot;max-width: 448px;&quot; /&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;In this post, we’ll start with the backend, which is a small node.js service that provides a RESTful API for managing the task list. In particular, we’ll:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;cover some (opinionated) background on web application testing,&lt;/li&gt;
  &lt;li&gt;set up a node.js service and a database for development with Docker Compose,&lt;/li&gt;
  &lt;li&gt;write shell scripts to automate some repetitive &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose&lt;/code&gt; commands,&lt;/li&gt;
  &lt;li&gt;see how to set up and connect to separate databases for development and test, and&lt;/li&gt;
  &lt;li&gt;finally, run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm test&lt;/code&gt; in the container!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Subsequent posts will extend the approach developed here to include frontend and end-to-end testing, and then to multiple services. The Compose setup in this post is pretty standard and provides the foundation from which we’ll build up to using some more advanced features as the application grows.&lt;/p&gt;

&lt;p&gt;The code is &lt;a href=&quot;https://github.com/jdleesmiller/todo-demo&quot;&gt;available on GitHub&lt;/a&gt;, and each post in the series has a git tag that marks the &lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/tree/todo-backend/todo&quot;&gt;corresponding code&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;the-todo-list-manager-backend&quot;&gt;The TODO List Manager Backend&lt;/h2&gt;

&lt;p&gt;Let’s start with a quick tour of the service we’ll be developing and testing. It uses &lt;a href=&quot;https://www.postgresql.org/&quot;&gt;PostgreSQL&lt;/a&gt; for the datastore and &lt;a href=&quot;https://expressjs.com/&quot;&gt;Express&lt;/a&gt; for the web server. For convenient database access, it uses &lt;a href=&quot;https://knexjs.org&quot;&gt;knex.js&lt;/a&gt; with &lt;a href=&quot;https://vincit.github.io/objection.js/&quot;&gt;Objection.js&lt;/a&gt; as the object-relational mapping layer.&lt;/p&gt;

&lt;p&gt;It has just one model: a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Task&lt;/code&gt; on the TODO list. Each task has an identifier and a description, which can’t be null and must have a sensible length. Here’s the code:&lt;/p&gt;

&lt;h4 id=&quot;srctaskjs&quot;&gt;&lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-backend/todo/src/task.js&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src/task.js&lt;/code&gt;&lt;/a&gt;&lt;/h4&gt;
&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Model&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;objection&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;./knex&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// ensure database connections are set up&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Task&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Model&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;get&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;tableName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;tasks&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;get&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;jsonSchema&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;

      &lt;span class=&quot;na&quot;&gt;properties&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;integer&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;minLength&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;maxLength&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;255&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;module&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;exports&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Task&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The service exposes a &lt;a href=&quot;https://en.wikipedia.org/wiki/Representational_state_transfer&quot;&gt;RESTful&lt;/a&gt; API for managing the tasks, which is implemented in the usual way:&lt;/p&gt;

&lt;h4 id=&quot;srcappjs&quot;&gt;&lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-backend/todo/src/app.js&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src/app.js&lt;/code&gt;&lt;/a&gt;&lt;/h4&gt;
&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;bodyParser&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;body-parser&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;express&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;express&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Task&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;./task&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;app&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;express&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;use&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;bodyParser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Is the service up?&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;/status&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;req&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;res&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;res&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;sendStatus&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;204&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// List tasks.&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;/api/tasks&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;req&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;res&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;tasks&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;orderBy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;res&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;tasks&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Create a new task.&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;/api/tasks&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;req&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;res&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;task&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;insert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;req&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;description&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;res&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;task&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;instanceof&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ValidationError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;res&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;400&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;message&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Check the id route param looks like a valid id.&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;param&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;req&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;res&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/^&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\d&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;+$/&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;req&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;res&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;sendStatus&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;404&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Complete a task (by deleting it from the task list).&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;delete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;/api/tasks/:id&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;req&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;res&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;deleteById&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;req&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;res&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;sendStatus&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;204&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;module&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;exports&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;app&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The API endpoints are relatively thin wrappers around the ORM, because there’s not much logic in a TODO list app, but we’ll still find some worthwhile things to test.&lt;/p&gt;

&lt;p&gt;Behind the scenes, there is also some boilerplate for database &lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-backend/todo/knexfile.js&quot;&gt;connection strings&lt;/a&gt;, &lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-backend/todo/src/knex.js&quot;&gt;database access&lt;/a&gt;, &lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/tree/todo-backend/todo/migrations&quot;&gt;database migrations&lt;/a&gt;, and &lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-backend/todo/server.js&quot;&gt;running the server&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id=&quot;dockerfile-and-compose-for-development&quot;&gt;Dockerfile and Compose for Development&lt;/h3&gt;

&lt;p&gt;Next let’s see how to get the service running in development. I’ve followed the approach in &lt;a href=&quot;/articles/2019/09/06/lessons-building-node-app-docker.html&quot;&gt;my previous post&lt;/a&gt; to set up &lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-backend/todo/Dockerfile&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt;&lt;/a&gt; that handles getting node and its dependencies installed, so here let’s focus on the Compose file:&lt;/p&gt;

&lt;h4 id=&quot;docker-composeyml&quot;&gt;&lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-backend/todo/docker-compose.yml&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose.yml&lt;/code&gt;&lt;/a&gt;&lt;/h4&gt;
&lt;div class=&quot;language-yml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;3.7&apos;&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;todo&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;.&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;development&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;npx nodemon server.js&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;depends_on&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;postgres&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;PORT&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;8080&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;8080:8080&apos;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;.:/srv/todo&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;todo_node_modules:/srv/todo/node_modules&lt;/span&gt;

  &lt;span class=&quot;na&quot;&gt;postgres&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;postgres:12&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;POSTGRES_HOST_AUTH_METHOD&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;trust&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;todo_node_modules&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;It’s short but fairly dense. Let’s break it down:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The Compose file defines two services, our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;todo&lt;/code&gt; API service and its database, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;postgres&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;todo&lt;/code&gt; service is built from the current directory, using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;development&lt;/code&gt; stage of the multi-stage Dockerfile, like &lt;a href=&quot;/articles/2019/09/06/lessons-building-node-app-docker.html#docker-for-dev-and-prod&quot;&gt;in my last post&lt;/a&gt;.&lt;/li&gt;
  &lt;li&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;command&lt;/code&gt; runs the service in the container under &lt;a href=&quot;https://nodemon.io/&quot;&gt;nodemon&lt;/a&gt;, so it will restart automatically when the code changes in development.&lt;/li&gt;
  &lt;li&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;todo&lt;/code&gt; service &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;depends_on&lt;/code&gt; the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;postgres&lt;/code&gt; database; this just ensures that the database is started whenever the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;todo&lt;/code&gt; service starts.&lt;/li&gt;
  &lt;li&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;todo&lt;/code&gt; service &lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-backend/todo/server.js#L5&quot;&gt;uses the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PORT&lt;/code&gt; environment variable&lt;/a&gt; to decide what port to listen on in the container, here &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;8080&lt;/code&gt;. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ports&lt;/code&gt; key then tells compose to expose port &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;8080&lt;/code&gt; in the container on port &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;8080&lt;/code&gt; on the host, so we can access the service on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;http://localhost:8080&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;volumes&lt;/code&gt; are set up to allow us to bind the service’s source files on the host into the container for fast edit-reload-test cycles, like in &lt;a href=&quot;/articles/2019/09/06/lessons-building-node-app-docker.html#the-node_modules-volume-trick&quot;&gt;my last post&lt;/a&gt;.&lt;/li&gt;
  &lt;li&gt;There’s not much to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;postgres&lt;/code&gt; service, because we’re using it as it comes. It’s worth noting that the image is fixed to version 12, which is the latest at the time of writing. It’s a good idea to fix a version (at least a &lt;a href=&quot;https://www.postgresql.org/support/versioning/&quot;&gt;major version&lt;/a&gt;) to avoid accidental upgrades.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;development-helper-scripts&quot;&gt;Development Helper Scripts&lt;/h3&gt;

&lt;p&gt;Now that Compose is set up, let’s see how to use it. While not strictly speaking required, it is often helpful to write some &lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/tree/todo-backend/todo/bin&quot;&gt;shell scripts&lt;/a&gt; to automate common tasks. This saves on typing and helps with consistency. The most interesting script is the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/up&lt;/code&gt; script, which handles the initial setup and can also be safely re-run to make sure you are up to date after pulling in remote changes:&lt;/p&gt;

&lt;h4 id=&quot;binup&quot;&gt;&lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-backend/todo/bin/up&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/up&lt;/code&gt;&lt;/a&gt;&lt;/h4&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;#!/usr/bin/env bash&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-e&lt;/span&gt;

docker-compose up &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; postgres

&lt;span class=&quot;nv&quot;&gt;WAIT_FOR_PG_ISREADY&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;while ! pg_isready --quiet; do sleep 1; done;&quot;&lt;/span&gt;
docker-compose &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;postgres bash &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$WAIT_FOR_PG_ISREADY&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;ENV &lt;span class=&quot;k&quot;&gt;in &lt;/span&gt;development &lt;span class=&quot;nb&quot;&gt;test
&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;# Create database for this environment if it doesn&apos;t already exist.&lt;/span&gt;
  docker-compose &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;postgres &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    su - postgres &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;psql &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$ENV&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; -c &apos;&apos; || createdb &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$ENV&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;

  &lt;span class=&quot;c&quot;&gt;# Run migrations in this environment.&lt;/span&gt;
  docker-compose run &lt;span class=&quot;nt&quot;&gt;--rm&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-e&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;NODE_ENV&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$ENV&lt;/span&gt; todo npx knex migrate:latest
&lt;span class=&quot;k&quot;&gt;done

&lt;/span&gt;docker-compose up &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Taking it from the top:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;set -e&lt;/code&gt; tells the script to abort on the first error, instead of continuing and getting into even deeper trouble. All shell scripts should start with this.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;It starts up the database and then runs the built-in postgres &lt;a href=&quot;https://www.postgresql.org/docs/current/app-pg-isready.html&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pg_isready&lt;/code&gt;&lt;/a&gt; utility in a loop, waiting until postgres finishes starting up (which is usually pretty quick, but not instant). If we didn’t do this, subsequent commands that need the database might fail sporadically.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Then it creates two databases, one called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;development&lt;/code&gt; and one called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;test&lt;/code&gt; and runs the &lt;a href=&quot;https://en.wikipedia.org/wiki/Schema_migration&quot;&gt;database migrations&lt;/a&gt; in each one. The application &lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-backend/todo/knexfile.js&quot;&gt;uses the connection string for the appropriate database&lt;/a&gt; depending on what &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NODE_ENV&lt;/code&gt; it is running in. More on this later.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Finally it brings up the rest of the application with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;up -d&lt;/code&gt;, which runs it detached, in the background.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The application is just an API at this point, so here’s what it looks like when exercised with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;curl&lt;/code&gt; on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;localhost:8080&lt;/code&gt; — an interface that only a developer could love:&lt;/p&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Create a task &apos;foo&apos;.&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;curl &lt;span class=&quot;nt&quot;&gt;--header&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Content-Type: application/json&apos;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--data&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;{&quot;description&quot;: &quot;foo&quot;}&apos;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  http://localhost:8080/api/tasks
&lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;task&quot;&lt;/span&gt;:&lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;description&quot;&lt;/span&gt;:&lt;span class=&quot;s2&quot;&gt;&quot;foo&quot;&lt;/span&gt;,&lt;span class=&quot;s2&quot;&gt;&quot;id&quot;&lt;/span&gt;:1&lt;span class=&quot;o&quot;&gt;}}&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# List the tasks, which now include &apos;foo&apos;.&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;curl http://localhost:8080/api/tasks
&lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;tasks&quot;&lt;/span&gt;:[&lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;id&quot;&lt;/span&gt;:1,&lt;span class=&quot;s2&quot;&gt;&quot;description&quot;&lt;/span&gt;:&lt;span class=&quot;s2&quot;&gt;&quot;foo&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;}]}&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Complete task &apos;foo&apos; by its ID.&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;curl &lt;span class=&quot;nt&quot;&gt;-X&lt;/span&gt; DELETE http://localhost:8080/api/tasks/1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;the-tests&quot;&gt;The Tests&lt;/h2&gt;

&lt;p&gt;Now that we’ve seen the service, let’s look at the tests. I’ve chosen two kinds of tests for the example service, &lt;em&gt;model tests&lt;/em&gt; and &lt;em&gt;integration tests&lt;/em&gt;. These terms are borrowed from &lt;a href=&quot;https://guides.rubyonrails.org/testing.html&quot;&gt;Ruby on Rails&lt;/a&gt;, which I think encourages an approach to testing that is sound for many kinds of web applications.&lt;/p&gt;

&lt;p&gt;Model tests test the ‘model’ layer of the &lt;a href=&quot;https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller&quot;&gt;Model-View-Controller&lt;/a&gt; (MVC) architecture, which most web applications follow to at least some degree. The model layer contains the application’s core business logic. The controllers are responsible for translating between the model layer and the view layer, which comprises, for most web applications, HTML or JSON responses to HTTP requests. Integration tests test that the models, controllers and views work together. In diagram form:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;/assets/todo-demo/todo-backend-tests.svg&quot;&gt;&lt;img src=&quot;/assets/todo-demo/todo-backend-tests.svg&quot; alt=&quot;From left to right, a database, then a service comprising models, controllers and views. Model tests encompass the database and the models. Integration tests encompass the database, models, controllers and views.&quot; /&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;Note that both model and integration tests have access to the database; the database is not mocked, because it is an essential part of the application. There are certainly cases where mocks are the right tool for the job, but I think the primary database is rarely one of them. I’ve included &lt;a href=&quot;#appendix-views-on-testing&quot;&gt;an appendix&lt;/a&gt; with some further discussion on this point.&lt;/p&gt;

&lt;h4 id=&quot;model-tests&quot;&gt;Model Tests&lt;/h4&gt;

&lt;p&gt;So, let’s see some examples of model tests. The example app doesn’t have much in the way of core business logic, so these particular model tests border on trivial. However, they illustrate at least one thing that model tests often do: they exercise the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Task&lt;/code&gt; model to check that some valid data can be inserted and some invalid data can’t be:&lt;/p&gt;

&lt;h4 id=&quot;testmodeltasktestjs&quot;&gt;&lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-backend/todo/test/model/task.test.js&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;test/model/task.test.js&lt;/code&gt;&lt;/a&gt;&lt;/h4&gt;
&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;assert&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;cleanup&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;../support/cleanup&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Task&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;../../src/task&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;describe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Task&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;beforeEach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;cleanup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;database&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;can be created with a valid description&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;description&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;repeat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;255&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;task&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;insert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;description&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;assert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;strictEqual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;must have a description&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;insert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&apos;&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;assert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fail&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;assert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;instanceof&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ValidationError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;assert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/should NOT be shorter than 1 characters/&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;must not have an overly long description&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;insert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;repeat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;assert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fail&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;assert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;instanceof&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ValidationError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;assert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/should NOT be longer than 255 characters/&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;A few remarks:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;The tests are written with &lt;a href=&quot;https://mochajs.org/&quot;&gt;mocha&lt;/a&gt; and the built-in &lt;a href=&quot;https://nodejs.org/api/assert.html&quot;&gt;node assertions&lt;/a&gt;. (It is usually worthwhile to use a library such as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;chai&lt;/code&gt; for more kinds of assertions, but I didn’t want to overload the example app with tons of libraries, and node’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assert&lt;/code&gt; does the job.)&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cleanup.database&lt;/code&gt; hook at the top is a &lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-backend/todo/test/support/cleanup.js&quot;&gt;helper function&lt;/a&gt; I wrote to clear out the database before each test. Compared with cleaning up more selectively, this is a brute force approach, but it does help avoid flakey tests by making sure each test starts with a clean slate.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;These model tests are for the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Task&lt;/code&gt; model, which is a model in the ORM sense of the word. However, model tests can test other kinds of code that aren’t coupled to the ORM and database, too. If your application affords some pure functions (woo hoo!) or plain objects (also good!), you can still test those in model tests. Just omit the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cleanup.database&lt;/code&gt; hook.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Technically, the main thing that distinguishes a model test from an integration test is that it doesn’t require spinning up or interacting with the express app. The model layer in MVC should be independent from the controller and view layers where possible — it should not care if it’s running in a background job or a service using websockets instead of plain HTTP. If it does, parts of it probably belong in the controller or view layer, where they will be easier to test with integration tests.&lt;/p&gt;

&lt;h4 id=&quot;integration-tests&quot;&gt;Integration tests&lt;/h4&gt;

&lt;p&gt;That brings us to integration tests, in which we do start up the express app and test it primarily by making HTTP requests. I wrote another &lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-backend/todo/test/support/test-server.js&quot;&gt;test helper&lt;/a&gt; to start the express application in a global mocha &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;before&lt;/code&gt; hook, so it starts once for the whole test suite. The helper also puts a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;testClient&lt;/code&gt; object on mocha’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;this&lt;/code&gt; with convenience methods for making requests against the app, such as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;this.testClient.post&lt;/code&gt;. Here are some integration tests:&lt;/p&gt;

&lt;h4 id=&quot;testintegrationtodotestjs&quot;&gt;&lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-backend/todo/test/integration/todo.test.js&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;test/integration/todo.test.js&lt;/code&gt;&lt;/a&gt;&lt;/h4&gt;
&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;assert&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;cleanup&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;../support/cleanup&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Task&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;../../src/task&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Ensure the global test server is started, for this.testClient.&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;../support/test-server&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;describe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;todo&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;beforeEach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;cleanup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;database&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;describe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;with existing tasks&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;exampleTasks&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;beforeEach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;exampleTasks&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;insert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;foo&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;bar&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;description&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;description&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}))&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;

    &lt;span class=&quot;nx&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;lists the tasks&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;testClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;/api/tasks&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;assert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ok&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;body&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;assert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;strictEqual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;tasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;length&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;assert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;strictEqual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;tasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;foo&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;

    &lt;span class=&quot;nx&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;completes a task&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;testClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;delete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;s2&quot;&gt;`/api/tasks/&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;exampleTasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;assert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;strictEqual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;204&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

      &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;remainingTasks&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;assert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;strictEqual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;remainingTasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;length&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;assert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;strictEqual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;remainingTasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;exampleTasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;creates a task&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;testClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;/api/tasks&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;foo&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;body&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;assert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;strictEqual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;foo&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;handles a validation error on create&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;testClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;/api/tasks&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{})&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;assert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ok&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;assert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;strictEqual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;400&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;body&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;assert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;strictEqual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;description: is a required property&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;handles an invalid task ID&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;testClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;delete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`/api/tasks/foo`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;assert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;strictEqual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;404&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;A few talking points:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;The tests generally follow a &lt;em&gt;state verification&lt;/em&gt; pattern, in which we put the system into an initial state, provide some input to the system, and then verify the output or the final state, or both. For example, the setup for the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;describe(&apos;with existing tasks&apos;, ...)&lt;/code&gt; block creates two tasks in the database, and then the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;it(&apos;completes a task&apos;, ...)&lt;/code&gt; test makes a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DELETE&lt;/code&gt; request and verifies that the service (1) produces the correct response code, &lt;a href=&quot;https://http.cat/204&quot;&gt;204&lt;/a&gt;, and (2) puts the database into the expected state, in which there is only one uncompleted task left.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The tests are &lt;a href=&quot;https://en.wikipedia.org/wiki/Gray_box_testing&quot;&gt;gray box&lt;/a&gt; tests, in that they are allowed to reach into the database (ideally through the model layer) to affect and inspect the state of the system. Here the API is complete enough that we could write these tests as &lt;a href=&quot;https://en.wikipedia.org/wiki/Black-box_testing&quot;&gt;black box&lt;/a&gt; tests using only the public API, but that is not always the case. Having access to the model layer in integration tests gives a lot of useful flexibility.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The integration tests aim to cover all the success and error handling cases in the app’s controllers, but they don’t exhaustively test all of the possible causes of errors in the model layer, because those are covered in the model tests. For example, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;it(&apos;handles a validation error on create&apos;, ...)&lt;/code&gt; test checks what happens if the description is missing, but there isn’t an integration test for the case where a description that is too long, because there was a model test for that.&lt;/p&gt;

    &lt;p&gt;This effect is a major contributor to the often talked about &lt;a href=&quot;https://martinfowler.com/articles/practical-test-pyramid.html&quot;&gt;test pyramid&lt;/a&gt;, in which we have more model tests than integration tests. In this example, the model layer is too simple for that pattern to emerge, but the test pyramid is a good ideal to strive for in a large application. Integration tests generally take longer to run and require more effort to write than model tests, because there is much more going on — making requests, receiving and deserializing responses, etc.. Other things being equal, it’s usually best to test at the lowest level you can.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;running-the-tests&quot;&gt;Running the Tests&lt;/h2&gt;

&lt;p&gt;With model and integration tests in hand, let’s see how to run them. We’ve seen above that the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/up&lt;/code&gt; script creates separate &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;development&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;test&lt;/code&gt; databases, so we have to set up the service to use them. This happens mainly in the &lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-backend/todo/knexfile.js&quot;&gt;knexfile&lt;/a&gt;:&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;common&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;postgresql&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;module&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;exports&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;development&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;common&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;connection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;postgres://postgres:postgres@postgres/development&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;common&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;connection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;postgres://postgres:postgres@postgres/test&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;production&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;common&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;connection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;process&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;DATABASE_URL&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The development and test connection strings tell the application how to connect to postgres as the default &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;postgres&lt;/code&gt; user, which has default password &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;postgres&lt;/code&gt;, running on the host &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;postgres&lt;/code&gt;, as declared in our Compose file. (It is a bit like that &lt;a href=&quot;https://en.wikipedia.org/wiki/Buffalo_buffalo_Buffalo_buffalo_buffalo_buffalo_Buffalo_buffalo&quot;&gt;buffalo buffalo sentence&lt;/a&gt;.) In production, we assume that the service will be provided with a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DATABASE_URL&lt;/code&gt; environment variable, because hard coding production credentials here would be a bad idea.&lt;/p&gt;

&lt;p&gt;Then in the service’s &lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-backend/todo/package.json&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;package.json&lt;/code&gt;&lt;/a&gt; we can set up the test scripts to run with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NODE_ENV=test&lt;/code&gt;, which is how the service knows to connect to the test database:&lt;/p&gt;
&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nl&quot;&gt;&quot;scripts&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;test&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;NODE_ENV=test mocha &apos;test/**/*.test.js&apos;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;test:model&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;NODE_ENV=test mocha &apos;test/model/**/*.test.js&apos;&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;test:model&lt;/code&gt; script runs only the model tests, which can be useful if you just want to run some model tests without the overhead of starting the express app in the global &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;before&lt;/code&gt; hook, as mentioned above.&lt;/p&gt;

&lt;p&gt;So, we are now ready to run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm test&lt;/code&gt; in a Docker container:&lt;/p&gt;
&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker-compose run &lt;span class=&quot;nt&quot;&gt;--rm&lt;/span&gt; todo npm &lt;span class=&quot;nb&quot;&gt;test
&lt;/span&gt;Starting todo_postgres_1 ... &lt;span class=&quot;k&quot;&gt;done&lt;/span&gt;

&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; todo@1.0.0 &lt;span class=&quot;nb&quot;&gt;test&lt;/span&gt; /srv/todo
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;NODE_ENV&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;test &lt;/span&gt;mocha &lt;span class=&quot;s1&quot;&gt;&apos;test/**/*.test.js&apos;&lt;/span&gt;



  todo
    ✓ creates a task &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;123ms&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    ✓ handles a validation error on create
    ✓ handles an invalid task ID
    with existing tasks
      ✓ lists the tasks
      ✓ completes a task

  Task
    ✓ can be created with a valid description
    ✓ must have a description
    ✓ must not have an overly long description


  8 passing &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;349ms&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;which is the name of the game for this blog post. (Or we can use the &lt;a href=&quot;https://github.com/jdleesmiller/todo-demo/blob/todo-backend/todo/bin/test&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/test&lt;/code&gt;&lt;/a&gt; helper script, which does the same thing.)&lt;/p&gt;

&lt;p&gt;Finally, it’s worth noting that to run a subset of the tests, mocha’s grep flag works, provided that it is after a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--&lt;/code&gt; delimiter to tell &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm&lt;/code&gt; to pass it through to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mocha&lt;/code&gt;. For example,&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ bin/test -- --grep Task
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;runs only the tests with names containing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Task&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;We’ve seen how to write and run model and integration tests for a simple node.js web service with Docker and Docker Compose. Model tests exercise the core business logic in the model layer, and integration tests check that the core business logic is correctly wired up to the controller and view layers. Both types of tests benefit from being able to access the database, which is not mocked.&lt;/p&gt;

&lt;p&gt;The Docker Compose setup for this project was pretty simple — just one container for the application. We’ll see some more advanced Docker Compose usage in subsequent posts, together with scripts like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/up&lt;/code&gt; to help drive them.&lt;/p&gt;

&lt;p&gt;Next time, we’ll add a frontend so we can manage our TODO list without &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;curl&lt;/code&gt;, and of course we will add some frontend tests. See you then!&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/articles/2020/01/12/testing-node-docker-compose-frontend.html&quot;&gt;Onward to part 2!&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;p&gt;If you’ve read this far, you should &lt;a href=&quot;https://twitter.com/jdleesmiller&quot;&gt;follow me on twitter&lt;/a&gt;, or maybe even apply to work at &lt;a href=&quot;https://www.overleaf.com&quot;&gt;Overleaf&lt;/a&gt;. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;h2 id=&quot;appendix-views-on-testing&quot;&gt;Appendix: Views on Testing&lt;/h2&gt;

&lt;p&gt;This series of posts makes some assumptions about the kinds of automated tests that we’re trying to write, so I should say what those assumptions are. I’ll start with the big picture and work back to the practical.&lt;/p&gt;

&lt;p&gt;So, why do we test? We test to &lt;em&gt;estimate correctness&lt;/em&gt;. Good testing lets us iterate quickly to improve correctness by providing accurate and cheap estimates.&lt;/p&gt;

&lt;p&gt;I say ‘estimate’ here because we can in principle measure ‘ground truth’ correctness by letting the system loose in the real world and seeing what happens. If we can release to production quickly and get feedback quickly through great monitoring, and if the cost of system failure is low, we might not need to estimate. For example, if we deliver pictures of cats at scale, we might just ship to production and measure; if we make antilock braking systems, not so much. In most domains, it is worth investing in testing so we can accurately predict and improve correctness before we go to production.&lt;/p&gt;

&lt;p&gt;The main way to achieve high prediction accuracy is through high test &lt;em&gt;fidelity&lt;/em&gt;. As they say at NASA, &lt;a href=&quot;http://llis.nasa.gov/lesson/1196&quot;&gt;fly as you test, test as you fly&lt;/a&gt;. For high fidelity, the system under test should closely resemble the one in production, and it should be tested in a way that closely resembles how it is used in production. However, fidelity usually comes at a cost.&lt;/p&gt;

&lt;p&gt;There are two main costs to testing: the effort to create and maintain the tests, and the time to run the tests. Both are important. Software systems and their requirements change frequently, which requires developers to spend time adding and updating tests. And those tests run many, many times, which leaves developers twiddling their thumbs while they wait for test results.&lt;/p&gt;

&lt;p&gt;Testing effectively requires finding the right tradeoff between fidelity and cost for your domain. This dynamic drives many decisions in testing. One example in this post is the decision to write both model tests and integration tests. We could just test everything with integration tests, which would be high fidelity but also high cost. Model tests are lower fidelity, in that they only test a part of the system, but they are generally easier to write and faster to run than integration tests, and hence lower cost.&lt;/p&gt;

&lt;p&gt;The use of fakes (test doubles / mocks / stubs / etc.) is another important example; it can reduce test runtimes at the expense of lower fidelity and more effort to create and maintain the fakes. If replacing a component with a fake makes tests for other components much easier to write or faster to run without too much loss of fidelity, and it is not too much work to create and maintain the fake, it can be a good tradeoff.&lt;/p&gt;

&lt;p&gt;Generally it makes sense to fake a component when two conditions hold: it is slow or unwieldy, and its coupling with the rest of the system is low. For example, if you want to test that your system sends an email when a user registers, you don’t need to stand up a full SMTP server and email client in your test environment; it is better to just &lt;a href=&quot;https://guides.rubyonrails.org/testing.html#testing-your-mailers&quot;&gt;fake the code&lt;/a&gt; that sends the email. Third party APIs called over HTTP often fall into this category, too, especially because there are good tools, such as &lt;a href=&quot;https://github.com/vcr/vcr&quot;&gt;vcr&lt;/a&gt; and &lt;a href=&quot;https://netflix.github.io/pollyjs&quot;&gt;polly.js&lt;/a&gt;, that make it easy to fake them.&lt;/p&gt;

&lt;p&gt;One component I think it seldom makes sense to fake is the database. In most applications, coupling with the database is high, simply because some business logic is best handled by the database. The humble uniqueness constraint, for example, is very difficult to achieve without race conditions in application code but trivial with help from the database. And if you are fortunate enough to have a database that can do joins and enforce some basic data integrity constraints, you should definitely let it do that instead of endlessly rewriting that logic in application code. Given this coupling, including the database in the system under test increases fidelity and reduces costs to write and maintain tests, at the expense of longer test runtimes.&lt;/p&gt;

&lt;p&gt;Fortunately, there are other ways to decrease test runtimes besides using fakes. A key property of testing is that it is &lt;a href=&quot;https://en.wikipedia.org/wiki/Embarrassingly_parallel&quot;&gt;embarrassingly parallel&lt;/a&gt;. Tests cases are independent by design, so it is straightforward (though not necessarily trivial) to run them in parallel. Some frameworks, &lt;a href=&quot;https://guides.rubyonrails.org/testing.html#parallel-testing&quot;&gt;such as rails&lt;/a&gt;, can do this out of the box, and scalable Continuous Integration services make it easy to bring a lot of compute power to bear on your tests at relatively low cost. Of course, massive parallelism doesn’t help so much if you are running the tests on your laptop, but in most cases you don’t have to rerun the whole test suite to make progress on a single feature — usually only a subset of the tests will be relevant. Then you can push the code to the beefy CI boxes to get a final check that the change hasn’t broken something in an unexpected part of the application.&lt;/p&gt;

&lt;p&gt;So, to sum up, in this series of blog posts I’m advocating for high fidelity (don’t fake too much) and low costs to write and maintain tests (don’t spend too much time tending fakes), with high investment in testing infrastructure to offset longer test runtimes. This does certainly make test environments more complex, but Docker and Docker Compose provide many useful tools for managing this complexity, which is one of the motivations for this series of blog posts.&lt;/p&gt;
</description>
        <pubDate>Sat, 19 Oct 2019 22:00:00 +0000</pubDate>
        <link>http://jdlm.info/articles/2019/10/19/testing-node-docker-compose-backend.html</link>
        <guid isPermaLink="true">http://jdlm.info/articles/2019/10/19/testing-node-docker-compose-backend.html</guid>
        
        
        <category>articles</category>
        
      </item>
    
      <item>
        <title>Lessons from Building Node Apps in Docker (2019)</title>
        <description>&lt;p&gt;&lt;strong&gt;Update (2019-10-10):&lt;/strong&gt; This post was &lt;a href=&quot;https://news.ycombinator.com/item?id=21209216&quot;&gt;discussed on Hacker News&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Way back in 2016, I wrote &lt;a href=&quot;/articles/2016/03/06/lessons-building-node-app-docker.html&quot;&gt;Lessons from Building a Node App in Docker&lt;/a&gt;, which has now helped over a hundred thousand people Dockerize their node.js apps. Since then there have been many changes, both in the ecosystem and how I work with node in Docker, so it was due for an overhaul.&lt;/p&gt;

&lt;p&gt;In this updated tutorial, we’ll set up the &lt;a href=&quot;http://socket.io/get-started/chat/&quot;&gt;socket.io chat example&lt;/a&gt; with Docker, from scratch to production-ready. In particular, we’ll see how to:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Get started bootstrapping a node application with Docker.&lt;/li&gt;
  &lt;li&gt;Not run everything as root (bad!).&lt;/li&gt;
  &lt;li&gt;Use binds to keep your test-edit-reload cycle short in development.&lt;/li&gt;
  &lt;li&gt;Manage &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node_modules&lt;/code&gt; in a container (there’s a trick to this).&lt;/li&gt;
  &lt;li&gt;Ensure repeatable builds with &lt;a href=&quot;https://docs.npmjs.com/files/package-lock.json&quot;&gt;package-lock.json&lt;/a&gt;.&lt;/li&gt;
  &lt;li&gt;Share a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt; between development and production using multi-stage builds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This tutorial assumes you already have some familiarity with Docker and node. If you’d like a gentle intro to Docker first, I’d recommend running through &lt;a href=&quot;https://docs.docker.com/get-started/&quot;&gt;Docker’s official introduction&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id=&quot;getting-started&quot;&gt;Getting Started&lt;/h3&gt;

&lt;p&gt;We’re going to set things up from scratch. The final code is available &lt;a href=&quot;https://github.com/jdleesmiller/docker-chat-demo&quot;&gt;on github here&lt;/a&gt;, and there are tags for each step along the way. &lt;a href=&quot;https://github.com/jdleesmiller/docker-chat-demo/tree/2019-01-bootstrapping&quot;&gt;Here’s the code for the first step&lt;/a&gt;, in case you’d like to follow along.&lt;/p&gt;

&lt;p&gt;Without Docker, we’d start by installing node and any other dependencies on the host and running &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm init&lt;/code&gt; to create a new package. There’s nothing stopping us from doing that here, but we’ll learn more if we use Docker from the start. (And of course the whole point of using Docker is that you don’t have to install things on the host.) We’ll start by creating a “bootstrapping container” that has node installed, and we’ll use it to set up the npm package for the application.&lt;/p&gt;

&lt;h4 id=&quot;the-bootstrapping-container-and-service&quot;&gt;The Bootstrapping Container and Service&lt;/h4&gt;

&lt;p&gt;We’ll need to write two files, a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt; and a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose.yml&lt;/code&gt;, to which we’ll add more later on. Let’s start with the bootstrapping &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-docker highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; node:10.16.3&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;USER&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; node&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;WORKDIR&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; /srv/chat&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;It’s a short file, but there already some important points:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;It starts from the official Docker image for the latest long term support (LTS) node release, at time of writing. I prefer to name a specific version, rather than one of the ‘floating’ tags like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node:lts&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node:latest&lt;/code&gt;, so that if you or someone else builds this image on a different machine, they will get the same version, rather than risking an accidental upgrade and attendant head-scratching.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;USER&lt;/code&gt; step tells Docker to run any subsequent build steps, and later the process in the container, as the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node&lt;/code&gt; user, which is an unprivileged user that comes built into all of the official node images from Docker. Without this line, they would run as &lt;strong&gt;root&lt;/strong&gt;, which is against security best practices and in particular the &lt;a href=&quot;https://en.wikipedia.org/wiki/Principle_of_least_privilege&quot;&gt;principle of least privilege&lt;/a&gt;. Many Docker tutorials skip this step for simplicity, and we will have to do some extra work to avoid running as root, but I think it’s very important.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WORKDIR&lt;/code&gt; step sets the working directory for any subsequent build steps, and later for containers created from the image, to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/srv/chat&lt;/code&gt;, which is where we’ll put our application files. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/srv&lt;/code&gt; folder should be available on any system that follows the &lt;a href=&quot;https://refspecs.linuxfoundation.org/fhs.shtml&quot;&gt;Filesystem Hierarchy Standard&lt;/a&gt;, which says that it is for “site-specific data which is served by this system”, which sounds like a good fit for a node app &lt;sup id=&quot;fnref:srv&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:srv&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now let’s move on to the bootstrapping compose file, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose.yml&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;3.7&apos;&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;chat&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;.&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;echo &apos;ready&apos;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;.:/srv/chat&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Again there is quite a bit to unpack:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;version&lt;/code&gt; line tells Docker Compose which version of its &lt;a href=&quot;https://docs.docker.com/compose/compose-file&quot;&gt;file format&lt;/a&gt; we are using. Version 3.7 is the latest at the time of writing, so I’ve gone with that, but older 3.x and 2.x versions would also work fine here; in fact, the 2.x series might even be a better fit, depending on your use case &lt;sup id=&quot;fnref:compose-file-v2&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:compose-file-v2&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The file defines a single service called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;chat&lt;/code&gt;, built from the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt; in the current directory, denoted &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.&lt;/code&gt;. All the service does for now is to echo &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ready&lt;/code&gt; and exit.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The volume line, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.:/srv/chat&lt;/code&gt;, tells Docker to bind mount the current directory &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.&lt;/code&gt; on the host at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/srv/chat&lt;/code&gt; in the container, which is the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WORKDIR&lt;/code&gt; we set up in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt; above. This means that changes we’ll make to source files on the host will be automatically reflected inside the container, and vice versa. This is very important for keeping your test-edit-reload cycles as short as possible in development. It will, however, create some issues with how npm installs dependencies, which we’ll come back to shortly.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now we’re ready to build and test our bootstrapping container. When we run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose build&lt;/code&gt;, Docker will create an image with node set up as specified in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt;. Then &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose up&lt;/code&gt; will start a container with that image and run the echo command, which shows that everything is set up OK.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker-compose build
Building chat
Step 1/3 : FROM node:10.16.3
&lt;span class=&quot;c&quot;&gt;# ... more build output ...&lt;/span&gt;
Successfully built d22d841c07da
Successfully tagged docker-chat-demo_chat:latest

&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker-compose up
Creating docker-chat-demo_chat_1 ... &lt;span class=&quot;k&quot;&gt;done
&lt;/span&gt;Attaching to docker-chat-demo_chat_1
chat_1  | ready
docker-chat-demo_chat_1 exited with code 0
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This output indicates that the container ran, echoed &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ready&lt;/code&gt; and exited successfully. 🎉&lt;/p&gt;

&lt;h4 id=&quot;initializing-an-npm-package&quot;&gt;Initializing an npm package&lt;/h4&gt;

&lt;blockquote&gt;
  &lt;p&gt;⚠️ Aside for Linux users: For this next step to work smoothly, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node&lt;/code&gt; user in the container should have the same &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uid&lt;/code&gt; (user identifier) as your user on the host. This is because the user in the container needs to have permissions to read and write files on the host via the bind mount, and vice versa. I’ve included &lt;a href=&quot;#appendix-dealing-with-uid-mismatches-on-linux&quot;&gt;an appendix with advice on how to deal with this issue&lt;/a&gt;. Docker for Mac users don’t have to worry about it because of some uid remapping magic behind the scenes, but Docker for Linux users get much better performance, so I’d call it a draw.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now we have a node environment set up in Docker, we’re ready to set up the initial npm package files. To do this, we’ll run an interactive shell in the container for the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;chat&lt;/code&gt; service and use it to set up the initial package files:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker-compose run &lt;span class=&quot;nt&quot;&gt;--rm&lt;/span&gt; chat bash
node@467aa1c96e71:/srv/chat&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;npm init &lt;span class=&quot;nt&quot;&gt;--yes&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# ... writes package.json ...&lt;/span&gt;
node@467aa1c96e71:/srv/chat&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;npm &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# ... writes package-lock.json ...&lt;/span&gt;
node@467aa1c96e71:/srv/chat&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;exit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And then the files appear on the host, ready for us to commit to version control:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;tree
&lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt;
├── Dockerfile
├── docker-compose.yml
├── package-lock.json
└── package.json
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Here’s the &lt;a href=&quot;https://github.com/jdleesmiller/docker-chat-demo/tree/2019-02-bootstrapped&quot;&gt;resulting code on github&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;installing-dependencies&quot;&gt;Installing Dependencies&lt;/h2&gt;

&lt;p&gt;Next up on our list is to install the app’s dependencies. We want these dependencies to be installed inside the container via the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt;, so the container will contain everything needed to run the application. This means we need to get the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;package.json&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;package-lock.json&lt;/code&gt; files into the image and run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm install&lt;/code&gt; in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt;. Here’s what that change looks like:&lt;/p&gt;

&lt;div class=&quot;language-diff highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;gh&quot;&gt;diff --git a/Dockerfile b/Dockerfile
index b18769e..d48e026 100644
&lt;/span&gt;&lt;span class=&quot;gd&quot;&gt;--- a/Dockerfile
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+++ b/Dockerfile
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;@@ -1,5 +1,14 @@&lt;/span&gt;
 FROM node:10.16.3

+RUN mkdir /srv/chat &amp;amp;&amp;amp; chown node:node /srv/chat
&lt;span class=&quot;gi&quot;&gt;+
&lt;/span&gt; USER node

 WORKDIR /srv/chat
&lt;span class=&quot;gi&quot;&gt;+
+COPY --chown=node:node package.json package-lock.json ./
+
+RUN npm install --quiet
+
+# TODO: Can remove once we have some dependencies in package.json.
+RUN mkdir -p node_modules
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And here’s the explanation:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RUN&lt;/code&gt; step with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mkdir&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;chown&lt;/code&gt; commands, which are the only commands we need to run as root, creates the working directory and makes sure that it’s owned by the node user.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;It’s worth noting that there are two shell commands chained together in that single &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RUN&lt;/code&gt; step. Compared to splitting out the commands over multiple &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RUN&lt;/code&gt; steps, chaining them reduces the number of layers in the resulting image. In this example, it really doesn’t matter very much, but it is a &lt;a href=&quot;https://docs.docker.com/develop/develop-images/dockerfile_best-practices/&quot;&gt;good habit&lt;/a&gt; not to use more layers than you need. It can save a lot of disk space and download time if you e.g. download a package, unzip it, build it, install it, and then clean up in one step, rather than saving layers with all of the intermediate files for each step.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;COPY&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;./&lt;/code&gt; copies the npm packaging files to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WORKDIR&lt;/code&gt; that we set up above. The trailing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/&lt;/code&gt; tells Docker that the destination is a folder. The reason for copying in only the packaging files, rather than the whole application folder, is that Docker will cache the results of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm install&lt;/code&gt; step below and rerun it only if the packaging files change. If we copied in all our source files, changing any one would bust the cache even though the required packages had not changed, leading to unnecessary &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm install&lt;/code&gt;s in subsequent builds.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--chown=node:node&lt;/code&gt; flag for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;COPY&lt;/code&gt; ensures that the files are owned by the unprivileged &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node&lt;/code&gt; user rather than root, which is the default &lt;sup id=&quot;fnref:build-as-root&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:build-as-root&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm install&lt;/code&gt; step will run as the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node&lt;/code&gt; user in the working directory to install the dependencies in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/srv/chat/node_modules&lt;/code&gt; inside the container.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This last step is what we want, but it causes a problem in development when we bind mount the application folder on the host over &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/srv/chat&lt;/code&gt;. Unfortunately, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node_modules&lt;/code&gt; folder doesn’t exist on the host, so the bind effectively hides the node modules that we installed in the image. The final &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mkdir -p node_modules&lt;/code&gt; step and the next section are related to how we deal with this.&lt;/p&gt;

&lt;h3 id=&quot;the-node_modules-volume-trick&quot;&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node_modules&lt;/code&gt; Volume Trick&lt;/h3&gt;

&lt;p&gt;There are &lt;a href=&quot;https://github.com/docker/example-voting-app/blob/7629961971ab5ca9fdfeadff52e7127bd73684a5/result-app/Dockerfile#L8&quot;&gt;several&lt;/a&gt; &lt;a href=&quot;http://bitjudo.com/blog/2014/03/13/building-efficient-dockerfiles-node-dot-js/&quot;&gt;ways&lt;/a&gt; &lt;a href=&quot;https://semaphoreci.com/community/tutorials/dockerizing-a-node-js-web-application&quot;&gt;around&lt;/a&gt; the node modules hiding problem, but I think the most elegant is to &lt;a href=&quot;http://stackoverflow.com/questions/30043872/docker-compose-node-modules-not-present-in-a-volume-after-npm-install-succeeds&quot;&gt;use a volume&lt;/a&gt; within the bind to contain &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node_modules&lt;/code&gt;. To do this, we have to add a few lines to our docker compose file:&lt;/p&gt;

&lt;div class=&quot;language-diff highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;gh&quot;&gt;diff --git a/docker-compose.yml b/docker-compose.yml
index c9a2543..799e1f6 100644
&lt;/span&gt;&lt;span class=&quot;gd&quot;&gt;--- a/docker-compose.yml
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+++ b/docker-compose.yml
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;@@ -6,3 +6,7 @@&lt;/span&gt; services:
     command: echo &apos;ready&apos;
     volumes:
       - .:/srv/chat
&lt;span class=&quot;gi&quot;&gt;+      - chat_node_modules:/srv/chat/node_modules
+
+volumes:
+  chat_node_modules:
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;chat_node_modules:/srv/chat/node_modules&lt;/code&gt; volume line sets up a &lt;em&gt;named volume&lt;/em&gt; &lt;sup id=&quot;fnref:named-volume&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:named-volume&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;4&lt;/a&gt;&lt;/sup&gt; called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;chat_node_modules&lt;/code&gt; that contains the directory &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/srv/chat/node_modules&lt;/code&gt; in the container. The top level &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;volumes:&lt;/code&gt; section at the end must declare all named volumes, so we add &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;chat_node_modules&lt;/code&gt; there, too.&lt;/p&gt;

&lt;p&gt;So, it’s simple to do, but there is quite a bit going on behind the scenes to make it work:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;During the build, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm install&lt;/code&gt; installs the dependencies (which we’ll add in the next section) into &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/srv/chat/node_modules&lt;/code&gt; within the image. We’ll color the files from the image blue:
    &lt;pre style=&quot;color: blue;&quot;&gt;
/srv/chat$ tree # in image
.
├── node_modules
│   ├── accepts
...
│   └── yeast
├── package-lock.json
└── package.json
&lt;/pre&gt;
  &lt;/li&gt;
  &lt;li&gt;When we later start a container from that image using our compose file, Docker first binds the application folder from the host inside the container under &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/srv/chat&lt;/code&gt;. We’ll color the files from the host red:
    &lt;pre style=&quot;color: red;&quot;&gt;
/srv/chat$ tree # in container without node_modules volume
.
├── Dockerfile
├── docker-compose.yml
├── node_modules
├── package-lock.json
└── package.json
&lt;/pre&gt;
    &lt;p&gt;The bad news is that the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node_modules&lt;/code&gt; in the image are hidden by the bind; inside the container, we instead see only an empty &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node_modules&lt;/code&gt; folder on the host.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;However, we’re not done yet. Docker next creates a &lt;em&gt;volume&lt;/em&gt; that contains a copy of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/srv/chat/node_modules&lt;/code&gt; in the image, and it mounts it in the container. This, in turn, hides the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node_modules&lt;/code&gt; from the bind on the host:
    &lt;pre style=&quot;color: red;&quot;&gt;
/srv/chat$ tree # in container with node_modules volume
.
├── Dockerfile
├── docker-compose.yml&lt;div style=&quot;color: blue;&quot;&gt;├── node_modules
│   ├── accepts
...
│   └── yeast&lt;/div&gt;├── package-lock.json
└── package.json
&lt;/pre&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This gives us what we want: our source files on the host are bound inside the container, which allows for fast changes, and the dependencies are also available inside of the container, so we can use them to run the app.&lt;/p&gt;

&lt;p&gt;We can also now explain the final &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mkdir -p node_modules&lt;/code&gt; step in the bootstrapping &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt; above: we have not actually installed any packages yet, so &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm install&lt;/code&gt; doesn’t create the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node_modules&lt;/code&gt; folder during the build. When Docker creates the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/srv/chat/node_modules&lt;/code&gt; volume, it will automatically create the folder for us, but it will be owned by root, which means the node user won’t be able to write to it. We can preempt that by creating &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node_modules&lt;/code&gt; as the node user during the build. Once we have some packages installed, we no longer need this line.&lt;/p&gt;

&lt;h3 id=&quot;package-installation&quot;&gt;Package Installation&lt;/h3&gt;

&lt;p&gt;So, let’s rebuild the image, and we’ll be ready to install packages.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker-compose build
... builds and runs npm &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;with no packages yet&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The chat app requires express, so let’s get a shell in the container and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm install&lt;/code&gt; it with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--save&lt;/code&gt; to save the dependency to our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;package.json&lt;/code&gt; and update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;package-lock.json&lt;/code&gt; accordingly:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker-compose run &lt;span class=&quot;nt&quot;&gt;--rm&lt;/span&gt; chat bash
Creating volume &lt;span class=&quot;s2&quot;&gt;&quot;docker-chat-demo_chat_node_modules&quot;&lt;/span&gt; with default driver
node@241554e6b96c:/srv/chat&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;npm &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--save&lt;/span&gt; express
&lt;span class=&quot;c&quot;&gt;# ...&lt;/span&gt;
node@241554e6b96c:/srv/chat&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;exit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;package-lock.json&lt;/code&gt; file, which has for most purposes replaced the older &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm-shrinkwrap.json&lt;/code&gt; file, is important for ensuring that Docker image builds are repeatable. It records the versions of all direct and indirect dependencies and ensures that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm install&lt;/code&gt;s in Docker builds on different machines will all get the same dependency tree.&lt;/p&gt;

&lt;p&gt;Finally, it’s worth noting that the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node_modules&lt;/code&gt; we installed are not present on the host. There may be an empty &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node_modules&lt;/code&gt; folder on the host, which is a side effect of the binds and volumes we created, but the actual files live in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;chat_node_modules&lt;/code&gt; volume. If we run another shell in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;chat&lt;/code&gt; container, we’ll find them there:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls &lt;/span&gt;node_modules
&lt;span class=&quot;c&quot;&gt;# nothing on the host&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker-compose run &lt;span class=&quot;nt&quot;&gt;--rm&lt;/span&gt; chat bash
node@54d981e169de:/srv/chat&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-l&lt;/span&gt; node_modules/
total 196
drwxr-xr-x 2 node node 4096 Aug 25 20:07 accepts
&lt;span class=&quot;c&quot;&gt;# ... many node modules in the container&lt;/span&gt;
drwxr-xr-x 2 node node 4096 Aug 25 20:07 vary
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The next time we run a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose build&lt;/code&gt;, the modules will be installed into the image.&lt;/p&gt;

&lt;p&gt;Here’s the &lt;a href=&quot;https://github.com/jdleesmiller/docker-chat-demo/tree/2019-03-dependencies&quot;&gt;resulting code on github&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;running-the-app&quot;&gt;Running the App&lt;/h2&gt;

&lt;p&gt;We are finally ready to install the app, so we’ll copy in &lt;a href=&quot;https://github.com/socketio/chat-example&quot;&gt;the remaining source files&lt;/a&gt;, namely &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index.js&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index.html&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then we’ll install the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;socket.io&lt;/code&gt; package. At the time of writing, the chat example is only compatible with socket.io version 1, so we need to request version 1:&lt;/p&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker-compose run &lt;span class=&quot;nt&quot;&gt;--rm&lt;/span&gt; chat npm &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--save&lt;/span&gt; socket.io@1
&lt;span class=&quot;c&quot;&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In our docker compose file, we then remove our dummy &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;echo ready&lt;/code&gt; command and instead run the chat example server. Finally, we tell Docker Compose to export 3000 in the container on the host, so we can access it in a browser:&lt;/p&gt;

&lt;div class=&quot;language-diff highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;gh&quot;&gt;diff --git a/docker-compose.yml b/docker-compose.yml
index 799e1f6..ff92767 100644
&lt;/span&gt;&lt;span class=&quot;gd&quot;&gt;--- a/docker-compose.yml
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+++ b/docker-compose.yml
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;@@ -3,7 +3,9 @@&lt;/span&gt; version: &apos;3.7&apos;
 services:
   chat:
     build: .
&lt;span class=&quot;gd&quot;&gt;-    command: echo &apos;ready&apos;
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+    command: node index.js
+    ports:
+      - &apos;3000:3000&apos;
&lt;/span&gt;     volumes:
       - .:/srv/chat
       - chat_node_modules:/srv/chat/node_modules
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then we are ready to run with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose up&lt;/code&gt; &lt;sup id=&quot;fnref:no-build&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:no-build&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;5&lt;/a&gt;&lt;/sup&gt;:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker-compose up
Recreating dockerchatdemo_chat_1
Attaching to dockerchatdemo_chat_1
chat_1 | listening on &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;:3000
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then you can see it running on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;http://localhost:3000&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/docker_chat_demo/chat.png&quot; alt=&quot;Docker chat demo working!&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Here’s the &lt;a href=&quot;https://github.com/jdleesmiller/docker-chat-demo/tree/2019-04-the-app&quot;&gt;resulting code on github&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;docker-for-dev-and-prod&quot;&gt;Docker for Dev and Prod&lt;/h2&gt;

&lt;p&gt;We now have our app running in development under docker compose, which is pretty cool! Before we can use this container in production, we have a few problems to solve and possible improvements to make:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;Most importantly, the container as we’re building it at the moment does not actually contain the source code for the application — it just contains the npm packaging files and dependencies. The main idea of a container is that it should contain everything needed to run the application, so clearly we will want to change this.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/srv/chat&lt;/code&gt; application folder in the image is currently owned and writeable by the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node&lt;/code&gt; user. Most applications don’t need to rewrite their source files at runtime, so again applying the principle of least privilege, we shouldn’t let them.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The image is fairly large, weighing in at 909MB according to the handy &lt;a href=&quot;https://github.com/wagoodman/dive&quot;&gt;dive&lt;/a&gt; image inspection tool. It’s not worth obsessing over image size, but we don’t want to be needlessly wasteful either. Most of the image’s heft comes from the default &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node&lt;/code&gt; base image, which includes a full compiler tool chain that lets us build node modules that use native code (as opposed to pure JavaScript). We won’t need that compiler tool chain at runtime, so from both a security and performance point of view, it would be better not to ship it to production.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fortunately, Docker provide a powerful tool that helps with all of the above: multi-stage builds. The main idea is that we can have multiple &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FROM&lt;/code&gt; commands in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt;, one per stage, and each stage can copy files from previous stages. Let’s see how to set that up:&lt;/p&gt;

&lt;div class=&quot;language-diff highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;gh&quot;&gt;diff --git a/Dockerfile b/Dockerfile
index d48e026..6c8965d 100644
&lt;/span&gt;&lt;span class=&quot;gd&quot;&gt;--- a/Dockerfile
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+++ b/Dockerfile
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;@@ -1,4 +1,4 @@&lt;/span&gt;
&lt;span class=&quot;gd&quot;&gt;-FROM node:10.16.3
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+FROM node:10.16.3 AS development
&lt;/span&gt;
 RUN mkdir /srv/chat &amp;amp;&amp;amp; chown node:node /srv/chat

@@ -10,5 +10,14 @@ COPY --chown=node:node package.json package-lock.json ./

 RUN npm install --quiet

-# TODO: Can remove once we have some dependencies in package.json.
&lt;span class=&quot;gd&quot;&gt;-RUN mkdir -p node_modules
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+FROM node:10.16.3-slim AS production
+
+USER node
+
+WORKDIR /srv/chat
+
+COPY --from=development --chown=root:root /srv/chat/node_modules ./node_modules
+
+COPY . .
+
+CMD [&quot;node&quot;, &quot;index.js&quot;]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;Our existing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt; steps will form the first stage, which we’ll now give the name &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;development&lt;/code&gt; by adding &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AS development&lt;/code&gt; to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FROM&lt;/code&gt; line at the start. I’ve now removed the temporary &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mkdir -p node_modules&lt;/code&gt; step needed during bootstrapping, since we now have some packages installed.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The new second stage starts with the second &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FROM&lt;/code&gt; step, which pulls in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;slim&lt;/code&gt; node base image for the same node version and calls the stage &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;production&lt;/code&gt; for clarity. This &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;slim&lt;/code&gt; image is also an &lt;a href=&quot;https://hub.docker.com/_/node&quot;&gt;official node image&lt;/a&gt; from Docker. As its name suggests, it is smaller than the default &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node&lt;/code&gt; image, mainly because it doesn’t include the compiler toolchain; it includes only the system dependencies needed to run a node application, which are far fewer than what may be required to build one.&lt;/p&gt;

    &lt;p&gt;This multi-stage &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt; runs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm install&lt;/code&gt; in the first stage, which has the full node image at its disposal for the build. Then it copies the resulting &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node_modules&lt;/code&gt; folder to the second stage image, which uses the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;slim&lt;/code&gt; base image. This technique reduces the size of the production image from 909MB to 152MB, which is about a factor of 6 saving for relatively little effort &lt;sup id=&quot;fnref:alpine&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:alpine&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;6&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Again the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;USER node&lt;/code&gt; command tells Docker to run the build and the application as the unprivileged &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node&lt;/code&gt; user rather than as root. We also have to repeat the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WORKDIR&lt;/code&gt;, because it doesn’t persist into the second stage automatically.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;COPY --from=development --chown=root:root ...&lt;/code&gt; line copies the dependencies installed in the preceding &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;development&lt;/code&gt; stage into the production stage and makes them owned by root, so the node user can read but not write them.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;COPY . .&lt;/code&gt; line then copies the rest of the application files from the host to the working directory in the container, namely &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/srv/chat&lt;/code&gt;.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Finally, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CMD&lt;/code&gt; step specifies the command to run. In the development stage, the application files came from bind mounts set up with docker-compose, so it made sense to specify the command in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose.yml&lt;/code&gt; file instead of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt;. Here it makes more sense to specify the command in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt;, which builds it into the container.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now that we have our multi-stage &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt; set up, we need to tell Docker Compose to use only the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;development&lt;/code&gt; stage rather than going through the full &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt;, which we can do with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;target&lt;/code&gt; option:&lt;/p&gt;

&lt;div class=&quot;language-diff highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;gh&quot;&gt;diff --git a/docker-compose.yml b/docker-compose.yml
index ff92767..2ee0d9b 100644
&lt;/span&gt;&lt;span class=&quot;gd&quot;&gt;--- a/docker-compose.yml
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+++ b/docker-compose.yml
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;@@ -2,7 +2,9 @@&lt;/span&gt; version: &apos;3.7&apos;

 services:
   chat:
&lt;span class=&quot;gd&quot;&gt;-    build: .
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+    build:
+      context: .
+      target: development
&lt;/span&gt;     command: node index.js
     ports:
       - &apos;3000:3000&apos;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This will preserve the old behavior we had before we added multistage builds, in development.&lt;/p&gt;

&lt;p&gt;Finally, to make the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;COPY . .&lt;/code&gt; step in our new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt; safe, we should add a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.dockerignore&lt;/code&gt; file. Without it, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;COPY . .&lt;/code&gt; may pick up other things we don’t need or want in our production image, such as our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.git&lt;/code&gt; folder, any &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node_modules&lt;/code&gt; that are installed on the host outside of Docker, and indeed all the Docker-related files that go into building the image. Ignoring these leads to smaller images and also faster builds, because the Docker daemon does not have to work as hard to create its copy of the files for its build context. Here’s the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.dockerignore&lt;/code&gt; file:&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;.dockerignore
.git
docker-compose*.yml
Dockerfile
node_modules
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;With all of that set up, we can run a production build to simulate how a CI system might build the final image, and then run it like an orchestrator might:&lt;/p&gt;
&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker build &lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-t&lt;/span&gt; chat:latest
&lt;span class=&quot;c&quot;&gt;# ... build output ...&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker run &lt;span class=&quot;nt&quot;&gt;--rm&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--detach&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--publish&lt;/span&gt; 3000:3000 chat:latest
dd1cf2bf9496edee58e1f5122756796999942fa4437e289de4bd67b595e95745
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;and again access it in the browser on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;http://localhost:3000&lt;/code&gt;. When finished, we can stop it using the container ID from the command above &lt;sup id=&quot;fnref:signals&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:signals&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;7&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker stop dd1cf2bf9496edee58e1f5122756796999942fa4437e289de4bd67b595e95745
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;setting-up-nodemon-in-development&quot;&gt;Setting up &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nodemon&lt;/code&gt; in Development&lt;/h3&gt;

&lt;p&gt;Now that we have distinct development and production images, let’s see how to make the development image a bit more developer-friendly by running the application under &lt;a href=&quot;https://github.com/remy/nodemon&quot;&gt;nodemon&lt;/a&gt; for automatic reloads within the container when we change a source file. After running&lt;/p&gt;
&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker-compose run &lt;span class=&quot;nt&quot;&gt;--rm&lt;/span&gt; chat npm &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--save-dev&lt;/span&gt; nodemon
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;to install nodemon, we can update the compose file to run it:&lt;/p&gt;
&lt;div class=&quot;language-diff highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;gh&quot;&gt;diff --git a/docker-compose.yml b/docker-compose.yml
index 2ee0d9b..173a297 100644
&lt;/span&gt;&lt;span class=&quot;gd&quot;&gt;--- a/docker-compose.yml
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+++ b/docker-compose.yml
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;@@ -5,7 +5,7 @@&lt;/span&gt; services:
     build:
       context: .
       target: development
&lt;span class=&quot;gd&quot;&gt;-    command: node index.js
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+    command: npx nodemon index.js
&lt;/span&gt;     ports:
       - &apos;3000:3000&apos;
     volumes:
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Here we use &lt;a href=&quot;https://blog.npmjs.org/post/162869356040/introducing-npx-an-npm-package-runner&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npx&lt;/code&gt;&lt;/a&gt; to run nodemon through npm &lt;sup id=&quot;fnref:npm&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:npm&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;8&lt;/a&gt;&lt;/sup&gt;. When we bring up the service, we should see the familiar &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nodemon&lt;/code&gt; output &lt;sup id=&quot;fnref:nodemon-rs&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:nodemon-rs&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;9&lt;/a&gt;&lt;/sup&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;docker-compose up
Recreating docker-chat-demo_chat_1 ... done
Attaching to docker-chat-demo_chat_1
chat_1  | [nodemon] 1.19.2
chat_1  | [nodemon] to restart at any time, enter `rs`
chat_1  | [nodemon] watching dir(s): *.*
chat_1  | [nodemon] starting `node index.js`
chat_1  | listening on *:3000
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Finally, it’s worth noting that with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt; above the dev dependencies will be included in the production image. It is possible to break out another stage to avoid this, but I would argue it is not necessarily a bad thing to include them. Nodemon is unlikely to be wanted in production, it is true, but dev dependencies often include testing utilities, and including those means we can run the tests in our production container as part of CI. It also generally improves dev-prod parity, and as some &lt;a href=&quot;http://llis.nasa.gov/lesson/1196&quot;&gt;wise people once said&lt;/a&gt;, ‘test as you fly, fly as you test.’ Speaking of which, we don’t have any tests, but it’s easy enough to run them when we do:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker-compose run &lt;span class=&quot;nt&quot;&gt;--rm&lt;/span&gt; chat npm &lt;span class=&quot;nb&quot;&gt;test&lt;/span&gt;

&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; chat@1.0.0 &lt;span class=&quot;nb&quot;&gt;test&lt;/span&gt; /srv/chat
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Error: no test specified&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;exit &lt;/span&gt;1

Error: no &lt;span class=&quot;nb&quot;&gt;test &lt;/span&gt;specified
npm ERR! Test failed.  See above &lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;more details.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Here’s the &lt;a href=&quot;https://github.com/jdleesmiller/docker-chat-demo&quot;&gt;final code on github&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;We’ve taken an app and got it running in development and production entirely within Docker. Great job!&lt;/p&gt;

&lt;p&gt;We jumped through some hopefully edifying hoops to bootstrap a node environment without installing anything on the host. We also jumped through some hoops to avoid running builds and processes as root, instead running them as an unprivileged user for better security.&lt;/p&gt;

&lt;p&gt;Node / npm’s habit of putting dependencies in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node_modules&lt;/code&gt; subfolder makes our lives a little bit more complicated than other solutions, such as ruby’s bundler, that install your dependencies outside the application folder, but we were able to work around that fairly easily with the nested node modules volume trick.&lt;/p&gt;

&lt;p&gt;Finally, we used Docker’s multi-stage build feature to produce a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt; suitable for both development and production. This simple but powerful feature is useful in a wide variety of situations, and we’ll see it again in some future articles.&lt;/p&gt;

&lt;p&gt;My next article in this series will pick up where we left off about testing node.js services in Docker. See you then!&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/articles/2019/10/19/testing-node-docker-compose-backend.html&quot;&gt;Onward to the series about testing!&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;p&gt;Thanks to &lt;a href=&quot;https://twitter.com/h0peth0mas&quot;&gt;Hope Thomas&lt;/a&gt;, &lt;a href=&quot;https://twitter.com/40_thieves&quot;&gt;Ali Smith&lt;/a&gt; and &lt;a href=&quot;https://twitter.com/mserranom&quot;&gt;Miguel Serrano&lt;/a&gt; for reviewing drafts of this article.&lt;/p&gt;

&lt;p&gt;If you’ve read this far, you should &lt;a href=&quot;https://twitter.com/jdleesmiller&quot;&gt;follow me on twitter&lt;/a&gt;, or maybe even apply to work at &lt;a href=&quot;https://www.overleaf.com&quot;&gt;Overleaf&lt;/a&gt;. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;h1 id=&quot;appendix-dealing-with-uid-mismatches-on-linux&quot;&gt;Appendix: Dealing with UID mismatches on Linux&lt;/h1&gt;

&lt;p&gt;When using bind mounts to share files between a Linux host and a container, you are likely to hit permissions problems if the numeric uid of the user in the container doesn’t match that of the user on the host. For example, files created on the host may not be readable or writable in the container, or vice versa.&lt;/p&gt;

&lt;p&gt;We can work around this, but first it’s worth noting that if your uid on the host is 1000, everything is fine for Dockerized development with node. This is because Docker’s official node images &lt;a href=&quot;https://github.com/nodejs/docker-node/blob/c66cc451670ba92c260ce7ea9956e9c9b91bad4d/10/stretch/Dockerfile#L3-L4&quot;&gt;use uid 1000&lt;/a&gt; for the node user. You can check your uid on the host by running the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id&lt;/code&gt; command, which prints it out. For example, mine currently says &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uid=1000(john) gid=1000(john) ...&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A uid of 1000 is fairly common, because it is the uid assigned by the ubuntu install process. If you can convince everyone on your team to set their uid to 1000, everything will work fine. If not, here are a couple of workarounds:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;Run the service as root in development by simply omitting the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;USER node&lt;/code&gt; step from the development stage of the Dockerfile (introduced in the &lt;a href=&quot;#docker-for-dev-and-prod&quot;&gt;Docker for Dev and Prod&lt;/a&gt; section). This ensures that the user in the container (root) will be able to read and write files on the host. If the user in the container creates any files, they’ll be owned as root on the host, but you can always fix that by running &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sudo chown -R your-user:your-group .&lt;/code&gt; on the host.&lt;/p&gt;

    &lt;p&gt;You can (and should) still run the process as an unprivileged user in production.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Use Dockerfile &lt;a href=&quot;https://docs.docker.com/engine/reference/builder/#arg&quot;&gt;build arguments&lt;/a&gt; to configure the UID and GID of the node user at build time. We can do this by adding a few lines to the development stage of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt;:&lt;/p&gt;
    &lt;div class=&quot;language-docker highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;node:10.16.3&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;development&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;ARG&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; UID=1000&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;ARG&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; GID=1000&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;RUN &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\
&lt;/span&gt;  usermod &lt;span class=&quot;nt&quot;&gt;--uid&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;UID&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt; node &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; groupmod &lt;span class=&quot;nt&quot;&gt;--gid&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;GID&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt; node &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\
&lt;/span&gt;  &lt;span class=&quot;nb&quot;&gt;mkdir&lt;/span&gt; /srv/chat &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;chown &lt;/span&gt;node:node /srv/chat

&lt;span class=&quot;c&quot;&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
    &lt;p&gt;This introduces two build args, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UID&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GID&lt;/code&gt;, which default to the existing value of 1000 if no arguments are given, and changes the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node&lt;/code&gt; user and group to use those IDs before creating any files as the user.&lt;/p&gt;

    &lt;p&gt;Each developer with a non-1000 uid/gid has to set these &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;args&lt;/code&gt; for Docker Compose. One way to do this is to use a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose.override.yml&lt;/code&gt; file that is not checked into version control (i.e. is in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.gitignore&lt;/code&gt;), to set the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;args&lt;/code&gt;, like this:&lt;/p&gt;
    &lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;3.7&apos;&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;chat&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;UID&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;500&apos;&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;GID&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;500&apos;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
    &lt;p&gt;In this example, the uid and gid in the container will be set to 500. There may be some easier ways of doing this &lt;a href=&quot;https://github.com/docker/compose/issues/2380&quot;&gt;one day&lt;/a&gt;. Again, these changes only need to be done in the development stage, not production.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h1 id=&quot;footnotes&quot;&gt;Footnotes&lt;/h1&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:srv&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;Fundamentally, it doesn’t matter where the files go in the container. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/opt&lt;/code&gt; would also be a very reasonable choice. Another option would be to keep them under &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/home/node&lt;/code&gt;, which simplifies some file permissions management in development but requires more typing and makes less sense in production, where I’ll advocate letting root own the application files as a way of keeping them read only. In any case, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/srv&lt;/code&gt; will do. &lt;a href=&quot;#fnref:srv&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:compose-file-v2&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;Both the 2.x and 3.x versions of the Docker Compose file format are still being actively developed. The main benefit of the 3.x series is that it is cross-compatible between single-node applications running on Docker Compose and multi-node applications running on Docker Swarm. In order to be compatible, version 3 drops some useful features from version 2. If you are only interested in Docker Compose, you might prefer to stick with the &lt;a href=&quot;https://docs.docker.com/compose/compose-file/compose-file-v2/&quot;&gt;latest 2.x format&lt;/a&gt;. &lt;a href=&quot;#fnref:compose-file-v2&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:build-as-root&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;Some of this trickery in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt; can be removed if we allow the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm install&lt;/code&gt; build step to run as root. If we do, we can and should still use the unprivileged node user at runtime, which is where most of the security benefits reside. A &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt; to run the build as root and the container as the node user would look more like this:&lt;/p&gt;
      &lt;div class=&quot;language-docker highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; node:10.16.3&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;WORKDIR&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; /srv/chat&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;COPY&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; package.json package-lock.json ./&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;RUN &lt;/span&gt;npm &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--quiet&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;USER&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; node&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;      &lt;/div&gt;

      &lt;p&gt;This is cleaner, without the need for some &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mkdir&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;chown&lt;/code&gt; tricks, at the expense of running &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm install&lt;/code&gt; as root at build time. Overall, I think the modest increase in complexity is worth it to avoid running the build as root, but you might decide that you prefer the cleaner &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt;.&lt;/p&gt;

      &lt;p&gt;One caveat if you build as root is that when you want to later install new dependencies you need to run a shell as root instead of the node user, as in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose run --rm --user root chat bash&lt;/code&gt; and then &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm install --save express&lt;/code&gt;. This is a bit like “sudoing” to install packages, which is a fairly familiar experience. &lt;a href=&quot;#fnref:build-as-root&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:named-volume&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;We could instead use an &lt;em&gt;anonymous volume&lt;/em&gt; to contain the modules, just by omitting the name:&lt;/p&gt;
      &lt;div class=&quot;language-diff highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;gh&quot;&gt;diff --git a/docker-compose.yml b/docker-compose.yml
index c9a2543..5a56364 100644
&lt;/span&gt;&lt;span class=&quot;gd&quot;&gt;--- a/docker-compose.yml
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+++ b/docker-compose.yml
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;@@ -6,3 +6,4 @@&lt;/span&gt; services:
     command: echo &apos;ready&apos;
     volumes:
       - .:/srv/chat
&lt;span class=&quot;gi&quot;&gt;+      - /srv/chat/node_modules
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;      &lt;/div&gt;
      &lt;p&gt;That would be shorter, but it is very easy to forget to clean up anonymous volumes, which leads to a profusion of anonymous modules with no indication which container they came from. You can still clean them up with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker system prune&lt;/code&gt;, but that is a bit of a ‘sledge hammer to crack a nut’. The named volumes approach is a bit more verbose but also more transparent.&lt;/p&gt;

      &lt;p&gt;(Extra credit: you might wonder where those dependency files in the volume actually get stored. In short, whether using named or anonymous volumes, they live in a separate directory managed by Docker on the host; see the &lt;a href=&quot;https://docs.docker.com/storage/volumes/&quot;&gt;Docker docs about volumes&lt;/a&gt; for more info.) &lt;a href=&quot;#fnref:named-volume&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:no-build&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;The eagle eyed reader may have noticed that we don’t have to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose build&lt;/code&gt; to get the dependencies installed before &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose up&lt;/code&gt;. This is because it is running with the node modules in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;chat_node_modules&lt;/code&gt; named volume. The next time we do a build, npm will install the dependencies from scratch into the image, but for installing packages day-to-day, we can just run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm install&lt;/code&gt; in the container without having to rebuild.&lt;/p&gt;

      &lt;p&gt;If you ever find yourself in a situation where you want to get rid of the named volume and start from scratch, you can run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker volume list&lt;/code&gt; to get a list of all volumes. The full name of your node modules volume will depend on your docker compose project. In my case, the volume of interest is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-chat-demo_chat_node_modules&lt;/code&gt;, which can be removed if we first remove the container with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose rm -v chat&lt;/code&gt; and then the volume itself with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker volume rm docker-chat-demo_chat_node_modules&lt;/code&gt;. &lt;a href=&quot;#fnref:no-build&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:alpine&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;Docker also provides an official &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;alpine&lt;/code&gt; image variant that is even smaller. However, these size savings come in part from using a completely different &lt;a href=&quot;https://en.wikipedia.org/wiki/C_standard_library&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;libc&lt;/code&gt;&lt;/a&gt; and package manager than the Debian-based images. Unless you are deploying to embedded systems where space is at a premium, the complexities that may arise due to these differences may not be worth it, especially when the Debian-based slim images already offer substantial savings. &lt;a href=&quot;#fnref:alpine&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:signals&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;You might notice that it takes about 10s to stop. This is because the socket.io chat example does not correctly handle the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SIGTERM&lt;/code&gt; signal, which Docker sends it when it’s time to stop, to perform a graceful shutdown. Extra credit: add this code to the end of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index.js&lt;/code&gt;:&lt;/p&gt;
      &lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;process&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;SIGTERM&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;io&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;close&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;Object&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;values&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;io&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;of&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;connected&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;disconnect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;      &lt;/div&gt;
      &lt;p&gt;then rebuild the production image and try to run and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker stop&lt;/code&gt; the container again. It should disconnect any clients and stop promptly after that change. &lt;a href=&quot;#fnref:signals&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:npm&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;An alternative to using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npx&lt;/code&gt; would be to put the command in an npm script, such as for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm start&lt;/code&gt;. However, running npm scripts in a container can easily cause the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SIGTERM&lt;/code&gt; from Docker to get lost, leading to non-graceful (10s) shutdowns even when the child properly handles the signal; see &lt;sup id=&quot;fnref:signals:1&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:signals&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;7&lt;/a&gt;&lt;/sup&gt;. This is because older versions of npm did not &lt;a href=&quot;https://github.com/npm/npm/pull/10868&quot;&gt;pass the signal&lt;/a&gt; to the child, and, even though that’s now fixed, the child typically runs in a shell, and most shells (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bash&lt;/code&gt;, at least) do not pass signals to their children. One workaround is to use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;exec&lt;/code&gt; in the script, as in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;exec nodemon server.js&lt;/code&gt;, which replaces the shell process with the node process, but it’s easy to forget. Moreover, running a command through an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm&lt;/code&gt; script creates some overhead, namely an additional node process. So, other things being equal, it’s best to define long running commands in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt; (with the &lt;a href=&quot;https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#cmd&quot;&gt;exec form of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CMD&lt;/code&gt;&lt;/a&gt;) or Docker Compose files instead. For short-running scripts, such as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm test&lt;/code&gt;, where signal handling is not a concern, npm scripts are usually fine. &lt;a href=&quot;#fnref:npm&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:nodemon-rs&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;You may notice that nodemon says that typing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rs&lt;/code&gt; will restart it. That won’t work if we use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose up&lt;/code&gt; to bring up the service, because our terminal is not connected to nodemon’s standard input when we do that. If we run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose run --rm chat&lt;/code&gt; instead, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rs&lt;/code&gt; should work as usual; this can be useful when working on one service in particular. &lt;a href=&quot;#fnref:nodemon-rs&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</description>
        <pubDate>Fri, 06 Sep 2019 16:00:00 +0000</pubDate>
        <link>http://jdlm.info/articles/2019/09/06/lessons-building-node-app-docker.html</link>
        <guid isPermaLink="true">http://jdlm.info/articles/2019/09/06/lessons-building-node-app-docker.html</guid>
        
        
        <category>articles</category>
        
      </item>
    
      <item>
        <title>From Zero to Driverless Race Car with Deep Learning</title>
        <description>&lt;p&gt;This post is based on the following talk.&lt;/p&gt;

&lt;p&gt;Abstract:&lt;/p&gt;

&lt;blockquote&gt;
Learn how to train a driverless car to drive around a simulated race track using end-to-end deep learning &amp;mdash; from camera images to steering commands. Key techniques used include deep neural networks, data augmentation, and transfer learning. This was a course project, so I&apos;ll introduce the key ideas and talk about the practical steps needed to get it working. You&apos;ll also see a lot of very dangerous driving.
&lt;/blockquote&gt;

&lt;p&gt; &lt;/p&gt;

&lt;div style=&quot;position:relative;padding-top:56.25%;&quot;&gt;
  &lt;iframe src=&quot;https://www.youtube.com/embed/h_Ma-ZA-pP0&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot; allow=&quot;accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture&quot; style=&quot;position:absolute;top:0;left:0;width:100%;height:100%;&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;p&gt; &lt;/p&gt;

&lt;h1 id=&quot;introduction&quot;&gt;Introduction&lt;/h1&gt;

&lt;p&gt;Today I’m going to talk about how to build a driverless race car using deep learning.&lt;/p&gt;

&lt;p&gt;I should start by saying that this was a personal project, but it does have a connection to my work. By day, I’m CTO of &lt;a href=&quot;https://www.overleaf.com&quot;&gt;Overleaf&lt;/a&gt;, the online LaTeX editor. Overleaf now has over three million users, and the first two, namely my co-founder and I, were driverless car researchers! We started Overleaf while we were working on the &lt;a href=&quot;https://en.wikipedia.org/wiki/ULTra_(rapid_transit)#Heathrow_Terminal_5&quot;&gt;Heathrow Pod&lt;/a&gt;, which was the world’s first driverless taxi system. It launched in 2011, and it’s still running, so if you ever have some time at London’s Heathrow Airport, you should take a ride on the pods out to business parking and back to Terminal 5.&lt;/p&gt;

&lt;div style=&quot;columns: 2; column-width: 300px;&quot;&gt;
  &lt;p&gt;
    &lt;a href=&quot;/assets/driverless/02-overleaf.jpg&quot;&gt;
      &lt;img src=&quot;/assets/driverless/02-overleaf.jpg&quot; alt=&quot;Screenshot of the Overleaf home page&quot; style=&quot;border: 1pt solid grey;&quot; /&gt;
    &lt;/a&gt;
  &lt;/p&gt;
  &lt;p&gt;
    &lt;a href=&quot;/assets/driverless/03-heathrow-pod.jpg&quot;&gt;
      &lt;img src=&quot;/assets/driverless/03-heathrow-pod.jpg&quot; alt=&quot;Photo of a driverless taxi from the Heathrow Pod system&quot; style=&quot;border: 1pt solid grey;&quot; /&gt;
    &lt;/a&gt;
  &lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;The key thing allowed us to put driverless taxis into active service way back in 2011 is that the taxis run on their own roads. It’s a closed system, and we used a fairly traditional systems engineering approach to design and build the system and prove that it was safe.&lt;/p&gt;

&lt;p&gt;Nowadays most driverless car research is about how we get them to work safely on public roads, where we need to worry about human drivers and pedestrians and cyclists and traffic lights and stop signs, and lots of other messy things.&lt;/p&gt;

&lt;p&gt;One of the pioneers in this area is Sebastian Thrun, who was a professor at Stanford and leader of the winning team in the 2005 &lt;a href=&quot;https://en.wikipedia.org/wiki/DARPA_Grand_Challenge&quot;&gt;DARPA Grand Challenge&lt;/a&gt;, which in many ways inaugurated the modern driverless car era. He later left to start Udacity, one of the leading &lt;a href=&quot;https://en.wikipedia.org/wiki/Massive_open_online_course&quot;&gt;MOOC&lt;/a&gt; providers, and they launched a driverless car course in 2016 &lt;sup id=&quot;fnref:courses&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:courses&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;, the Self-Driving Car Engineer nano-degree. It sounded great and with my copious free time (!) I enrolled.&lt;/p&gt;

&lt;div style=&quot;columns: 2; column-width: 300px;&quot;&gt;
  &lt;p&gt;
    &lt;a href=&quot;/assets/driverless/04-udacity.jpg&quot;&gt;
      &lt;img src=&quot;/assets/driverless/04-udacity.jpg&quot; alt=&quot;Photo of a Sebastian Thrun with the Udacity driverless car&quot; style=&quot;border: 1pt solid grey;&quot; /&gt;
    &lt;/a&gt;
  &lt;/p&gt;
  &lt;p&gt;
    &lt;a href=&quot;/assets/driverless/05-nvidia.jpg&quot;&gt;
      &lt;img src=&quot;/assets/driverless/05-nvidia.jpg&quot; alt=&quot;First page of NVIDIA&apos;s End-to-End Learning for Self-Driving Cars&quot; style=&quot;border: 1pt solid grey;&quot; /&gt;
    &lt;/a&gt;
  &lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;This talk is based on one of the labs in that course, which is in turn based on a 2016 paper from NVIDIA, called &lt;a href=&quot;https://arxiv.org/abs/1604.07316&quot;&gt;End-to-End Learning for Self-Driving Cars&lt;/a&gt;. What the NVIDIA team showed is that it’s possible to take images from a front-mounted camera on a car, feed them into a convolutional neural network, which we’ll talk about, and have it produce steering commands to drive the car. So, just like you look at the road ahead and decide whether you need to steer left or right, they did the same thing with a neural network.&lt;/p&gt;

&lt;p&gt;What’s remarkable about this is that it’s very different from the systems engineering approach that we usually take in driverless car engineering. In that approach we break the overall problem of driving the car down into lots of subproblems, such as object detection, object classification, mapping, planning, etc., and have different subsystems responsible for each of those subproblems. The subsystems are then connected up to make the whole system. Here, however, we’re just going to train one big monolithic neural network, which will somehow drive the car. It feels a bit like magic.&lt;/p&gt;

&lt;p&gt;Our task for this lab was to reproduce this magic, and this talk follows steps I went through to do so, which were broadly:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Collect training data in a simulator by driving around the track manually.&lt;/li&gt;
  &lt;li&gt;Train a deep neural network using the camera images and steering angles.&lt;/li&gt;
  &lt;li&gt;Use the network to drive the car around the track (also in simulation).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the middle, I’m going to handwave quite a lot about some of the theory. You may wish to skip that part if you are already familiar with convolutional neural networks.&lt;/p&gt;

&lt;h1 id=&quot;collecting-training-data&quot;&gt;Collecting Training Data&lt;/h1&gt;

&lt;p&gt;The first thing we need to do is collect training data by driving the car around in the simulator. We’re going to train the neural network to imitate me, so here I am trying to drive mostly in the center of the road, like we want the neural network to do.&lt;/p&gt;

&lt;p&gt;You can see in the top right a ‘recording’ sign, which means that we’re recording the camera images and my steering angle for each frame, 10 times each second.&lt;/p&gt;

&lt;p style=&quot;position:relative;padding-top:56.25%;&quot;&gt;
  &lt;iframe src=&quot;https://www.youtube.com/embed/k3Gpww3RfeQ&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot; allow=&quot;accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture&quot; style=&quot;position:absolute;top:0;left:0;width:100%;height:100%;&quot;&gt;&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;You might notice that I’m not very good at this. One of the key reasons is that the only way to steer the car is with the left and right arrow keys. So, the first thing we need to do is take my raw steering inputs and clean them up by smoothing them.&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;/assets/driverless/08-smoothing.png&quot;&gt;
    &lt;img src=&quot;/assets/driverless/08-smoothing.png&quot; alt=&quot;Plot of steering angle over time, with raw input (spiky blue line) and the output of two different smoothing algorithms (red and green lines)&quot; style=&quot;border: 1pt solid grey;&quot; /&gt;
  &lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;The spiky blue line is the raw steering input from me pressing the arrow keys. The green and red lines are the results of applying exponential and Gaussian smoothing, respectively. I tried both kinds of smoothing, and the Gaussian smoothing turned out to work better when actually driving the car.&lt;/p&gt;

&lt;p&gt;The next hurdle to overcome in this training process is that if I only show the car how to drive in the center of the road, it will never gain any experience of what to do if it ever finds itself off-center. We can solve this problem by recording &lt;em&gt;recoveries&lt;/em&gt; in training, in which I stop the recording, drive off to the side of the road, start recording, and then drive back into the middle:&lt;/p&gt;

&lt;p style=&quot;position:relative;padding-top:56.25%;&quot;&gt;
  &lt;iframe src=&quot;https://www.youtube.com/embed/CwRsP-bvKHo&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot; allow=&quot;accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture&quot; style=&quot;position:absolute;top:0;left:0;width:100%;height:100%;&quot;&gt;&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;By doing this, we teach the car how to get back to the center of the road, hopefully without teaching it too much about driving off the road.&lt;/p&gt;

&lt;p&gt;After driving around the track several times in both directions and doing recoveries, I ended up with about 11 thousand frames of training data: &lt;sup id=&quot;fnref:wing-cameras&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:wing-cameras&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;table style=&quot;max-width: 20em; margin: 1em auto;&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Dataset&lt;/th&gt;
      &lt;th&gt;Rows (Frames)&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Normal&lt;/td&gt;
      &lt;td&gt;5,273&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Recoveries&lt;/td&gt;
      &lt;td&gt;6,037&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Total&lt;/td&gt;
      &lt;td&gt;11,310&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;That is in some sense quite a lot, but in the world of deep learning, it is not very much at all, and I think we would struggle to train a network from scratch using just these data. To get around that, we’ll use a technique called transfer learning.&lt;/p&gt;

&lt;h1 id=&quot;transfer-learning&quot;&gt;Transfer Learning&lt;/h1&gt;

&lt;p&gt;Transfer learning means that we take someone else’s network that they have already trained for some other task, extract a little bit of it, and repurpose it for our task.&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;/assets/driverless/11-inception.png&quot;&gt;
    &lt;img src=&quot;/assets/driverless/11-inception.png&quot; alt=&quot;Transfer learning: a picture of Google&apos;s Inception v3 network architecture, including two example images, one of a Siberian Husky and one of an Eskimo Dog&quot; style=&quot;border: 1pt solid grey;&quot; /&gt;
  &lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;Here we’re going to use the &lt;a href=&quot;https://arxiv.org/abs/1409.4842&quot;&gt;Inception v3&lt;/a&gt; network, which was trained by Google for an image classification competition. It takes as input an image, runs lots of computations, and outputs the class of the image — that is, what kind of thing the image shows. If you feed in the image on the left, it will tell you that that is a Siberian Husky, and if you feed in the image on the right, it will tell you that that is an Eskimo Dog. (It is interesting to note that some of the classifications are quite fine; I’m not sure I could tell the difference!)&lt;/p&gt;

&lt;p&gt;The Inception network is a very large — it has over 25 million parameters, which Google has trained with considerable effort and expense. So, how would we go about building on the work that they’ve done? The answer is surprisingly simple: we ‘lobotomize’ the network and just take the first few layers (indicated by the red dashed box below).&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;/assets/driverless/12-inception-prefix.png&quot;&gt;
    &lt;img src=&quot;/assets/driverless/12-inception-prefix.png&quot; alt=&quot;The inception architecture from the previous slide, with a red dashed box around the first few layers&quot; style=&quot;border: 1pt solid grey;&quot; /&gt;
  &lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;The reason this works is that these first few layers of the network, 44 layers to be precise, turn out to be relatively generic image processing stuff — different kinds of edge detectors, for example. It is the later stages of the full Inception network that encode the difference between Siberian Huskies and Eskimo Dogs, and all of the other image classes, which we don’t need. So, for our application, we’re just going to tack a relatively small neural network on to the end of our Inception prefix network:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;/assets/driverless/13-transfer-architecture.png&quot;&gt;
    &lt;img src=&quot;/assets/driverless/13-transfer-architecture.png&quot; alt=&quot;The first 44 layers of the inception network, which provide generic image processing, followed by a convolution and two fully connected layers, which we will train for our task&quot; style=&quot;border: 1pt solid grey;&quot; /&gt;
  &lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;That means that we only have to train these last few layers ourselves, leaving the pre-trained Inception prefix network alone, so we can hopefully get away with much less training data than if we started from scratch.&lt;/p&gt;

&lt;p&gt;I should add the architecture of our last three layers was chosen after some trial and error with even simpler architectures, and this was the simplest one that seemed to work.&lt;/p&gt;

&lt;h1 id=&quot;convolutional-neural-networks&quot;&gt;Convolutional Neural Networks&lt;/h1&gt;

&lt;p&gt;So, how does this network actually work? This is where I will talk about some of the theory. To build a convolutional neural network, which is the kind of deep neural network that this is, we need three basic building blocks:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Convolution&lt;/li&gt;
  &lt;li&gt;Resizing (in particular making the images smaller)&lt;/li&gt;
  &lt;li&gt;Activation Functions&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let’s take each of these things in turn. I’m going to use this camera frame as an example:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;/assets/driverless/14-example.png&quot;&gt;
    &lt;img src=&quot;/assets/driverless/14-example.png&quot; alt=&quot;Example camera frame from the front of the vehicle, showing the road, sky and some trees. It is fed into the neural network from the previous slide to produce a steering angle&quot; style=&quot;border: 1pt solid grey;&quot; /&gt;
  &lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;One way to look at it is that our overall aim for this network is to go from a camera frame at 320 by 160 pixels down to a single number, which is the steering angle that the car should apply when it sees this image.&lt;/p&gt;

&lt;p&gt;Let’s start with our first building block, convolution. Convolution is a simple idea but also a very general one. To use it, we define what is called a kernel (it has many names, but I will call it a kernel), which is a small matrix of numbers. Here we’ll use a 3 pixel by 3 pixel kernel.&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;/assets/driverless/15-convolution.png&quot;&gt;
    &lt;img src=&quot;/assets/driverless/15-convolution.png&quot; alt=&quot;Explanation of convolution with three example kernels: an identity kernel (just copies the image), an edge detection kernel and a blur kernel&quot; style=&quot;border: 1pt solid grey;&quot; /&gt;
  &lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;We start by lining up our 3 pixel by 3 pixel kernel with the 3 pixel by 3 pixel patch in the top left corn of the image. We then multiply each pixel in the image with the corresponding value from the kernel and add them up to give us the first pixel in the output image &lt;sup id=&quot;fnref:channels&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:channels&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;. Then we slide our kernel over the rest of the pixels in the input image, pixel by pixel, each time yielding one more pixel in the output image: &lt;sup id=&quot;fnref:sliding&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:sliding&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;4&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;/assets/driverless/16-convolution-sliding.gif&quot;&gt;
    &lt;img src=&quot;/assets/driverless/16-convolution-sliding.gif&quot; alt=&quot;Animation showing a 3 pixel by 3 pixel kernel sliding over an input image&quot; /&gt;
  &lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;This operation is very simple and can be done efficiently, but it is also very powerful. If you use a program like Photoshop, most of the tools in the image filters menu will be using a convolution under the hood, each using a different kernel. The identity kernel is not very interesting, because it just copies the input to the output, but we can also choose kernels for edge detection, blurs, sharpens, and much more.&lt;/p&gt;

&lt;p&gt;To use convolution in a neural network, the key insight is that instead of very carefully engineering these kernels ourselves, we let them be learned from the training data. And we let the network learn a &lt;em&gt;lot&lt;/em&gt; of different kernels. The prefix of the Inception network that we’re using has about 700 thousand parameters, many which are are kernel parameters, which Google has trained using 1.2 million images.&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;/assets/driverless/17-kernels.png&quot;&gt;
    &lt;img src=&quot;/assets/driverless/17-kernels.png&quot; alt=&quot;Reminder of the Inception prefix network, which has about 700 thousand parameters, which are mostly kernels&quot; style=&quot;border: 1pt solid grey;&quot; /&gt;
  &lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;When we run convolutions with so many kernels, we basically take one input image and produce lots of output images — one for each kernel. If we were to keep adding more and more images, we would run out of memory, so we also need our next building block: resizing.&lt;/p&gt;

&lt;div style=&quot;columns: 2; column-width: 300px;&quot;&gt;
  &lt;p&gt;
    &lt;a href=&quot;/assets/driverless/18-resizing.png&quot;&gt;
      &lt;img src=&quot;/assets/driverless/18-resizing.png&quot; alt=&quot;Example of resizing a 224 pixel by 224 pixel image to 112 pixels by 112 pixels using pooling, a form of downsampling&quot; style=&quot;border: 1pt solid grey;&quot; /&gt;
    &lt;/a&gt;
  &lt;/p&gt;
  &lt;p&gt;
    &lt;a href=&quot;/assets/driverless/19-flat-to-deep.png&quot;&gt;
      &lt;img src=&quot;/assets/driverless/19-flat-to-deep.png&quot; alt=&quot;Reminder of the Inception prefix network, this time with a schematic showing a large flat image becoming smaller and deeper as more channels are added with convolution and it is downsampled with pooling&quot; style=&quot;border: 1pt solid grey;&quot; /&gt;
    &lt;/a&gt;
  &lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;This does what it says on the tin: every few convolutions, we resize the image to make it smaller. There are lots of ways of doing this, such as &lt;a href=&quot;https://computersciencewiki.org/index.php/Max-pooling_/_Pooling&quot;&gt;max pooling&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;When we resize, we lose some spatial resolution, but we gain depth. It lets us take our flat input image, and by repeated convolutions and resizing, get to a sausage shaped ‘image’ that is lower resolution but also deeper. The network gains, in some handwavy sense, &lt;em&gt;understanding&lt;/em&gt; with this depth — it started out with a patch of image that was just pixels, and now it has mapped those pixels into a set of &lt;em&gt;features&lt;/em&gt; that can carry more meaning to help it solve the task at hand.&lt;/p&gt;

&lt;p&gt;Our final building block is the Activation Function, which is where the ‘neural’ in ‘neural network’ comes from.&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;/assets/driverless/20-neurons.png&quot;&gt;
    &lt;img src=&quot;/assets/driverless/20-neurons.png&quot; alt=&quot;Comparison of real neuron, with dendrites and an axon, with a mathematical sigmoid function&quot; style=&quot;border: 1pt solid grey;&quot; /&gt;
  &lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;A real neuron is an incredibly complicated thing with amazing dynamics and lots of interesting behaviors, but we are going to model it in a very simple way. Essentially, it has dendrites, which are ‘inputs’ connected to upstream neurons, and if those inputs add up to at least some threshold, this neuron is going to &lt;em&gt;activate&lt;/em&gt; and send a signal through its axon to downstream neurons.&lt;/p&gt;

&lt;p&gt;Mathematically, we can represent this as a very simple squashing function. If the sum of the inputs is negative, it outputs a value near zero, which means that it is not activated. If the sum of the inputs is positive, it returns a value near one, which means it is activated.&lt;/p&gt;

&lt;p&gt;These three building blocks are all we need. We just repeat them over and over again. It’s worth noting, however, that convolution (of the kind used here) is a linear operation. If it were not for the little bits of nonlinearity that we get from (most kinds of) resizing and the activation function, the composition of convolutions would collapse down to one big linear function. However, once we add these relatively tame non-linearities into the mix, we go from being able to represent only linear functions to being able to &lt;a href=&quot;https://en.wikipedia.org/wiki/Universal_approximation_theorem&quot;&gt;approximate any function&lt;/a&gt;, which is pretty amazing.&lt;/p&gt;

&lt;h1 id=&quot;completing-the-network&quot;&gt;Completing the Network&lt;/h1&gt;

&lt;p&gt;So, that’s the end of my theory bit. Let’s see what it does in practice using our example camera image. When we feed it through the first 44 layers of the Inception network, we get back 256 greyscale ‘feature images’. The width and height of each feature image are about a factor of 10 smaller than the input image, but there are many more of them — as noted above, the output is much smaller and deeper than the input.&lt;/p&gt;

&lt;p&gt;Here are nine feature images, starting mostly at random from feature image number 42, out of our stack of 256. Each image shows one of the responses that the network gives to our example input image. A light pixel means that the neuron at that point in the feature image is activated — it’s responding to some characteristic of the corresponding part of the input image. A dark pixel means it isn’t activated.&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;/assets/driverless/21-features-44.png&quot;&gt;
    &lt;img src=&quot;/assets/driverless/21-features-44.png&quot; alt=&quot;Sample of nine feature images after the input image has passed through the first 44 layers of the Inception network&quot; style=&quot;border: 1pt solid grey;&quot; /&gt;
  &lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;If we overlay the response with the input image, it becomes easier for us to interpret what they’re responding to:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;/assets/driverless/22-overlays-44.png&quot;&gt;
    &lt;img src=&quot;/assets/driverless/22-overlays-44.png&quot; alt=&quot;The same nine feature images, this time overlayed over the input image&quot; style=&quot;border: 1pt solid grey;&quot; /&gt;
  &lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;In feature image number 42 (top left), for example, we can see some neurons responding fairly strongly to the edges of the road. When driving, it is pretty important to know where the edges of the road are, so this feature image may be useful. Number 43 (top center) seems to be responding to the road surface, which may be similarly useful. It’s also picking up some of the background, but we can also see that number 48 (middle right) is responding mainly to the background. So, it seems like some combination of these feature images would give us useful information.&lt;/p&gt;

&lt;p&gt;It’s important to note that we didn’t tell this part of the network how to find features that might be helpful for our task. In fact, this part of the network was trained by Google for a completely different task, namely image classification, but it seems to have some features that look like they may be useful for our task, which is encouraging.&lt;/p&gt;

&lt;p&gt;This brings us back to the bit of the network that we actually train, which are the three layers that we’ve added at the end.&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;/assets/driverless/23-architecture.png&quot;&gt;
    &lt;img src=&quot;/assets/driverless/23-architecture.png&quot; alt=&quot;Reminder of the network architecture with our few custom layers&quot; style=&quot;border: 1pt solid grey;&quot; /&gt;
  &lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;Our first layer is another convolution. This is a ‘one by one’ convolution, which means a kernel size of 1 pixel by 1 pixel. A 1x1 convolution picks a set number of linear combinations of its input feature images; it is often used for &lt;a href=&quot;https://stats.stackexchange.com/questions/194142/what-does-1x1-convolution-mean-in-a-neural-network&quot;&gt;dimensionality reduction&lt;/a&gt;. This architectural choice is motivated by the discussion above about how some linear combination of features images 42, 43 and 48 seems like it might be pretty good at finding the road — we want to let the network pick the most useful combinations of the 256 feature images from above.&lt;/p&gt;

&lt;p&gt;In this example, we’ll pick 64 linear combinations of the 256 features. Here are some new feature images after that 1x1 convolution. They look similar, but they are generally smoother and brighter than the feature images before the 1x1 convolution.&lt;/p&gt;

&lt;div style=&quot;columns: 2; column-width: 300px;&quot;&gt;
  &lt;p&gt;
    &lt;a href=&quot;/assets/driverless/24-features-1x1.png&quot;&gt;
      &lt;img src=&quot;/assets/driverless/24-features-1x1.png&quot; alt=&quot;&quot; style=&quot;border: 1pt solid grey;&quot; /&gt;
    &lt;/a&gt;
  &lt;/p&gt;
  &lt;p&gt;
    &lt;a href=&quot;/assets/driverless/25-overlays-1x1.png&quot;&gt;
      &lt;img src=&quot;/assets/driverless/25-overlays-1x1.png&quot; alt=&quot;&quot; style=&quot;border: 1pt solid grey;&quot; /&gt;
    &lt;/a&gt;
  &lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;Again we can see some features responding strongly to the edges of the road, for example feature image number 45 (out of the 64 new feature images this time).&lt;/p&gt;

&lt;p&gt;Finally, we have two fully connected layers. From now on, we won’t be able to visualise the results so easily, because we flatten all of the outputs from the convolution into one large list of numbers. A fully connected layer then computes a set number of linear combinations of all of those numbers.&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;/assets/driverless/27-fully-connected.png&quot;&gt;
    &lt;img src=&quot;/assets/driverless/27-fully-connected.png&quot; alt=&quot;Diagram of two fully connected layers and corresponding schematic showing a weighted linear sum of inputs, then the activation function applied to that sum, then the output&quot; style=&quot;border: 1pt solid grey;&quot; /&gt;
  &lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;The diagram on the right shows a schematic for one of those linear combinations — that is, one neuron – in one fully connected layer. A fully connected layer consists of many such neurons, each connected to all of the outputs of the previous layer. Here the weights we train are the &lt;em&gt;w&lt;sub&gt;i&lt;/sub&gt;&lt;/em&gt;, and the inputs are the &lt;em&gt;x&lt;sub&gt;i&lt;/sub&gt;&lt;/em&gt;. And again we apply an activation function, here denoted &lt;em&gt;f&lt;/em&gt;, to each neuron to introduce some nonlinearity, so our two fully connected layers don’t just collapse down into one big linear function.&lt;/p&gt;

&lt;p&gt;The outputs of the first fully connected layer feed into the second fully connected layer, and the output of the second fully connected layer is, at last, our steering angle! With our network architecture set out, we are ready to start training.&lt;/p&gt;

&lt;h1 id=&quot;time-to-train&quot;&gt;Time to Train&lt;/h1&gt;

&lt;p&gt;Well, almost ready. Even just for three layers that we need to train, there are quite a few &lt;em&gt;hyperparameters&lt;/em&gt; that we need to set before we can fully define the network and the training scenario. How many kernels should we use in our 1x1 convolutions? How large should each fully connected layer be? What kind of smoothing should we do on the steering angle? And many more.&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;/assets/driverless/29-hyperparameters.png&quot;&gt;
    &lt;img src=&quot;/assets/driverless/29-hyperparameters.png&quot; alt=&quot;A python Dict with some of the hyperparameters to tune&quot; style=&quot;border: 1pt solid grey;&quot; /&gt;
  &lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;There are smart ways to search hyperparameters, but in this case I just tried all possible combinations in a large grid search. It takes a while to run the whole grid, but it’s just computation — set it off before bed, and by the time you get home from work, new data are waiting.&lt;/p&gt;

&lt;p&gt;Each point in the grid gives one network to train and evaluate. Then we can evaluate the performance of each of the networks to choose the best hyperparameter settings.&lt;/p&gt;

&lt;p&gt;Fortunately, the actual training is made very easy by great libraries, such as &lt;a href=&quot;https://keras.io/&quot;&gt;Keras&lt;/a&gt;. Here’s an example of the Keras training output for one of the networks:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Layer (type)                     Output Shape          Param #     Connected to
====================================================================================================
convolution2d_1 (Convolution2D)  (None, 17, 37, 64)    16448       convolution2d_input_1[0][0]
____________________________________________________________________________________________________
flatten_1 (Flatten)              (None, 40256)         0           convolution2d_1[0][0]
____________________________________________________________________________________________________
dense_1 (Dense)                  (None, 32)            1288224     flatten_1[0][0]
____________________________________________________________________________________________________
dense_2 (Dense)                  (None, 1)             33          dense_1[0][0]
====================================================================================================
Total params: 1304705
____________________________________________________________________________________________________
Epoch 1/30
27144/27144 [==============================] - 160s - loss: 79.0047 - val_loss: 0.0780
Epoch 2/30
27144/27144 [==============================] - 147s - loss: 25.4130 - val_loss: 0.0692
Epoch 3/30
27144/27144 [==============================] - 148s - loss: 8.4912 - val_loss: 0.0670
Epoch 4/30
27144/27144 [==============================] - 148s - loss: 2.9383 - val_loss: 0.0638
…
Epoch 12/30
27144/27144 [==============================] - 148s - loss: 0.0851 - val_loss: 0.0572
Epoch 13/30
27144/27144 [==============================] - 148s - loss: 0.0785 - val_loss: 0.0568
Epoch 14/30
27144/27144 [==============================] - 148s - loss: 0.0802 - val_loss: 0.0546
Epoch 15/30
27144/27144 [==============================] - 147s - loss: 0.0769 - val_loss: 0.0569
Epoch 16/30
27144/27144 [==============================] - 147s - loss: 0.0793 - val_loss: 0.0560
Epoch 17/30
27144/27144 [==============================] - 148s - loss: 0.0832 - val_loss: 0.0574
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;There’s a lot going on in this output, and I’d like to remark on a few things. At the start of the output, we have Keras’s summary of the model we’re training, which includes numbers of parameters to fit. Remember that we’re keeping the layers from the Inception network that Google trained completely fixed, so we’re only worried about training our three layers at the end of the network. &lt;sup id=&quot;fnref:bottleneck&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:bottleneck&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;5&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;In the second section, we have the training progress, which Keras prints as it goes. I’ve split the training data I collected into a training set (80%) and a validation set (20%). &lt;sup id=&quot;fnref:wing-cameras-keras&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:wing-cameras-keras&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;6&lt;/a&gt;&lt;/sup&gt; At each Epoch, Keras reports the mean absolute error in the network’s predictions on the training set (the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;loss&lt;/code&gt;) and the validation set (the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;val_loss&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;For each of our three layers, training starts with randomly initialized weights. As you might expect, the initial loss with random weights is pretty terrible, starting at around 79 &lt;sup id=&quot;fnref:units&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:units&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;7&lt;/a&gt;&lt;/sup&gt;. However, Keras uses that loss to refine the weights for the next epoch, feeds the training set through again, and sure enough the loss drops with each successive epoch. After 17 epochs, the loss is orders of magnitude lower, at 0.08. Training stops when the validation set loss, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;val_loss&lt;/code&gt; starts to increase — to run more epochs would likely lead to overfitting.&lt;/p&gt;

&lt;h1 id=&quot;run-the-model-with-lowest-validation-loss&quot;&gt;Run the Model with Lowest Validation Loss!&lt;/h1&gt;

&lt;p&gt;So, after repeating that training process on hundreds of networks for all our various hyperparameters, let’s hook up the network with the best validation set loss to the car and see how it drives!&lt;/p&gt;

&lt;p style=&quot;position:relative;padding-top:56.25%;&quot;&gt;
  &lt;iframe src=&quot;https://www.youtube.com/embed/NC4aqDdsPRE&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot; allow=&quot;accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture&quot; style=&quot;position:absolute;top:0;left:0;width:100%;height:100%;&quot;&gt;&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;We put the simulator into autonomous mode, then the car starts moving. I haven’t talked about throttle control, but basically it sets the throttle to keep the car at about 10mph in this case.&lt;/p&gt;

&lt;p&gt;We can see the car weaving, and then correcting, but it overcorrects, and eventually it runs off the road and crashes. Still, it is not a bad start — it was clearly trying to stay on the road, which is encouraging.&lt;/p&gt;

&lt;h1 id=&quot;try-lots-of-things&quot;&gt;Try Lots of Things…&lt;/h1&gt;

&lt;p&gt;And so the debugging begins. There were lots of arbitrary decisions not in my initial grid of hyperparameters, so I started by adding many more to the grid. For example:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Different loss functions — how should we measure the error? I tried mean squared error and mean absolute error; the latter seemed a bit better.&lt;/li&gt;
  &lt;li&gt;Different activation functions — I tried sigmoid, tanh and &lt;a href=&quot;https://en.wikipedia.org/wiki/Rectifier_(neural_networks)&quot;&gt;ReLU&lt;/a&gt;; tanh seemed a bit better.&lt;/li&gt;
  &lt;li&gt;Different &lt;a href=&quot;https://en.wikipedia.org/wiki/Regularization_(mathematics)&quot;&gt;regularization&lt;/a&gt; — penalizing the weights so that they do not become too large is a common technique to avoid overfitting, and I tried several different L2-regularization weights.&lt;/li&gt;
  &lt;li&gt;Different distributions for initial weights (reduce variance of Normal) — this did solve some convergence problems during training.&lt;/li&gt;
  &lt;li&gt;Different layer sizes — how many neurons in each hidden layer?&lt;/li&gt;
  &lt;li&gt;Different network architectures — try adding more layers? Or fewer layers?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, none of the things I tried really moved the needle. After thrashing around for a few days without getting anywhere fast, I stopped fiddling with the neural network and added a print statement to the control loop. That quickly revealed the actual problem:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;/assets/driverless/34-latency.png&quot;&gt;
    &lt;img src=&quot;/assets/driverless/34-latency.png&quot; alt=&quot;The real problem: latency. Face palm&quot; style=&quot;border: 1pt solid grey;&quot; /&gt;
  &lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;The controller was spending too much time processing each frame, so it was only actually able to steer about three times per second. If you imagine trying to steer, but you can only touch the wheel three times a second, it does seem pretty tough. Further investigation revealed that it was spending most of its time in the Inception prefix layers. One solution would have been to buy a faster laptop. However, it turned out that it was possible to use fewer inception layers, in particular the first 12 layers, instead of the first 44 (it looks like 7 in the diagram, but some are not visible):&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;/assets/driverless/35-inception-prefix-12.png&quot;&gt;
    &lt;img src=&quot;/assets/driverless/35-inception-prefix-12.png&quot; alt=&quot;The Inception v3 network with the first 12 layers highlighted with a red dashed box&quot; style=&quot;border: 1pt solid grey;&quot; /&gt;
  &lt;/a&gt;
&lt;/p&gt;

&lt;h1 id=&quot;with-lower-latency&quot;&gt;With Lower Latency&lt;/h1&gt;

&lt;p&gt;Let’s see how it does with lower latency (about 0.1s instead of 0.35s):&lt;/p&gt;

&lt;p style=&quot;position:relative;padding-top:56.25%;&quot;&gt;
  &lt;iframe src=&quot;https://www.youtube.com/embed/qpaK5cps1As&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot; allow=&quot;accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture&quot; style=&quot;position:absolute;top:0;left:0;width:100%;height:100%;&quot;&gt;&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;It now does much better. I’ve sped up the video, because it makes it much further. The weaving is much reduced, and it’s staying mostly in the middle. It’s not following a great ‘racing line’, but that’s probably because I didn’t follow a great racing line when I was training it.&lt;/p&gt;

&lt;p&gt;Eventually it gets to a turn where there are some trees ahead of it, and it doesn’t make the turn. Still, progress!&lt;/p&gt;

&lt;p&gt;The resolution for this problem was basically to add more training data — I drove around that corner a few more times, and eventually it learned to make it. At around the same time, Udacity released a bunch of training data from someone who had a steering wheel, instead of having to do the arrow keys and smoothing, so I added that in too.&lt;/p&gt;

&lt;p&gt;I also did some &lt;em&gt;augmentation&lt;/em&gt; with the training data, which in retrospect was fairly obvious: you can mirror every image in your original training set and negate the steering angle, and you have another training example.&lt;/p&gt;

&lt;h1 id=&quot;the-final-result&quot;&gt;The Final Result&lt;/h1&gt;

&lt;p&gt;With all this additional training data, the network managed the following:&lt;/p&gt;

&lt;p style=&quot;position:relative;padding-top:56.25%;&quot;&gt;
  &lt;iframe src=&quot;https://www.youtube.com/embed/3Nx3JwfGIBc&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot; allow=&quot;accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture&quot; style=&quot;position:absolute;top:0;left:0;width:100%;height:100%;&quot;&gt;&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;I changed the throttle control to let it drive at 30mph, which was the cap in this version of the Udacity simulator, so it’s going quite a bit faster. You can still see some ‘shimmy’ but it is no longer crashing. Success!&lt;/p&gt;

&lt;h1 id=&quot;does-it-generalize&quot;&gt;Does it Generalize?&lt;/h1&gt;

&lt;p&gt;So, we’ve seen that for this race track, we can train the network and make it go around. What if we put it on a totally different race track? Fortunately Udacity provided just a second track in the simulator, so let’s try it out. Note that this is exactly the same network as in the previous video, and it has not seen any part of this new track in its training data.&lt;/p&gt;

&lt;p style=&quot;position:relative;padding-top:56.25%;&quot;&gt;
  &lt;iframe src=&quot;https://www.youtube.com/embed/3ew8wv6Lhv0&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot; allow=&quot;accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture&quot; style=&quot;position:absolute;top:0;left:0;width:100%;height:100%;&quot;&gt;&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Behold, it drives! Again it shows sensitivity to trees, with a near miss at 0:24 and eventually a crash, both with trees in its field of view. Still, considering that this is a very simple network, as neural networks go, and it hasn’t had a huge amount of training, the fact that it does this well on an unseen track, albeit one in the same simulator, is I think pretty good.&lt;/p&gt;

&lt;h1 id=&quot;conclusions&quot;&gt;Conclusions&lt;/h1&gt;

&lt;p&gt;In this post we:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Took a neural network designed to classify pictures of dogs.&lt;/li&gt;
  &lt;li&gt;Repurposed it to drive a car.&lt;/li&gt;
  &lt;li&gt;Taught it to drive with ~35 minutes’ training.&lt;/li&gt;
  &lt;li&gt;Watched it drive around an unseen race track (for a little while).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s amazing!&lt;/p&gt;

&lt;p&gt;Moreover,&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;In 2015, very few people would have even thought this was possible.&lt;/li&gt;
  &lt;li&gt;In 2016, thousands of people like me were doing it in their spare time in an online course.&lt;/li&gt;
  &lt;li&gt;In 2018, I mentored (very slightly) Josh, a high school student who did it all himself on an RC car with a Raspberry Pi.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s also amazing!&lt;/p&gt;

&lt;div style=&quot;columns: 2; column-width: 300px;&quot;&gt;
  &lt;p&gt;
    &lt;a href=&quot;/assets/driverless/40-conclusion-1.png&quot;&gt;
      &lt;img src=&quot;/assets/driverless/40-conclusion-1.png&quot; alt=&quot;Conclusions as in the text, plus pictures of the dogs and the simulator.&quot; style=&quot;border: 1pt solid grey;&quot; /&gt;
    &lt;/a&gt;
  &lt;/p&gt;
  &lt;p&gt;
    &lt;a href=&quot;/assets/driverless/41-conclusion-2.png&quot;&gt;
      &lt;img src=&quot;/assets/driverless/41-conclusion-2.png&quot; alt=&quot;Conclusions as in the text, plus pictures of Sebastian Thrun, an RC car and a track for the RC car&quot; style=&quot;border: 1pt solid grey;&quot; /&gt;
    &lt;/a&gt;
  &lt;/p&gt;
&lt;/div&gt;

&lt;p&gt; &lt;/p&gt;

&lt;p&gt;I prepared this talk for the 2018 Holtzbrinck Publishing Group AI Day. I would like to thank the organizers for giving me the impetus to finally write this talk and for providing the video.&lt;/p&gt;

&lt;p&gt;The code for this project is &lt;a href=&quot;https://github.com/jdleesmiller/carnd-cloning&quot;&gt;open source&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you’ve read this far, perhaps you should &lt;a href=&quot;https://twitter.com/jdleesmiller&quot;&gt;follow me on twitter&lt;/a&gt;, or maybe even &lt;a href=&quot;https://www.overleaf.com/jobs&quot;&gt;join our team&lt;/a&gt;. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt; &lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;/assets/driverless/42-thanks.jpg&quot;&gt;
    &lt;img src=&quot;/assets/driverless/42-thanks.jpg&quot; alt=&quot;Photo of a Heathrow Pod vehicle at dusk in one of the business parking stations&quot; style=&quot;border: 1pt solid grey;&quot; /&gt;
  &lt;/a&gt;
&lt;/p&gt;

&lt;h1 id=&quot;footnotes&quot;&gt;Footnotes&lt;/h1&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:courses&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;Udacity actually ran two driverless car courses. The first was &lt;a href=&quot;https://eu.udacity.com/course/artificial-intelligence-for-robotics--cs373&quot;&gt;&lt;em&gt;CS373: Programming a Robotic Car&lt;/em&gt;&lt;/a&gt;, which I completed in 2012, and is still offered for free. The second was the &lt;a href=&quot;https://eu.udacity.com/course/self-driving-car-engineer-nanodegree--nd013&quot;&gt;&lt;em&gt;Self Driving Car Engineer&lt;/em&gt;&lt;/a&gt; nano-degree. Both were great! &lt;a href=&quot;#fnref:courses&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:wing-cameras&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;The simulator actually takes three camera images in each frame, one on the left wing, one in the center, and one on the right wing. I also added the wing cameras to the training data, together with an empirically determined adjustment to the target steering angle to account for the different wing camera position. In that sense the number of examples is 33,930 (3 times 11,310), but it’s not clear how much these images add, since they are very similar to the central camera image. I didn’t have time to go into this in my talk. &lt;a href=&quot;#fnref:wing-cameras&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:channels&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;Here I have glossed over an important detail: the input image is a color image, so it actually has three &lt;em&gt;channels&lt;/em&gt;, one each for red, green and blue (&lt;a href=&quot;https://en.wikipedia.org/wiki/RGB_color_model&quot;&gt;RGB&lt;/a&gt;). The convolutions in convolutional neural neworks operate on all of the channels in their input, so the kernels here should actually be thought of as three-dimensional kernels in this case — 3 pixels by 3 pixels by 3 channels. I did not have time to get into this detail in my talk (and drawing that would have been hard!). &lt;a href=&quot;#fnref:channels&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:sliding&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;This image is from &lt;a href=&quot;https://towardsdatascience.com/types-of-convolutions-in-deep-learning-717013397f4d&quot;&gt;An Introduction to different Types of Convolutions in Deep Learning&lt;/a&gt;, by Paul-Louis Pröve, which is an excellent place to find out more about convolution. &lt;a href=&quot;#fnref:sliding&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:bottleneck&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;Because the Inception prefix layers remain fixed through training, a useful optimisation is to run the training data through the prefix network once and save the outputs. The saved outputs are then fed into the training process for the later layers many times. Quite a lot of the code is concerned with doing this, but it does make a big difference to training times. The output features from the prefix are usually called ‘bottleneck’ features, and the slides with example features from the Inception prefix network are taken from these bottleneck features. &lt;a href=&quot;#fnref:bottleneck&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:wing-cameras-keras&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;The eagle-eyed reader may notice that Keras reports 27,144 images in the training set, which is larger than the 11,310 points I reported in my table. See &lt;sup id=&quot;fnref:wing-cameras:1&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:wing-cameras&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;. The size of the training set is 80 percent of 11,310 samples times 3 frames per sample, which is 27,144. &lt;a href=&quot;#fnref:wing-cameras-keras&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:units&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;That is not 79 degrees; the angle has been scaled so that the range [-1, 1] corresponds to [-25˚, 25˚], so it is effectively doing donuts during the first training epoch. &lt;a href=&quot;#fnref:units&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</description>
        <pubDate>Tue, 01 Jan 2019 16:30:00 +0000</pubDate>
        <link>http://jdlm.info/articles/2019/01/01/driverless-race-car-deep-learning.html</link>
        <guid isPermaLink="true">http://jdlm.info/articles/2019/01/01/driverless-race-car-deep-learning.html</guid>
        
        
        <category>articles</category>
        
      </item>
    
      <item>
        <title>The Mathematics of 2048: Optimal Play with Markov Decision Processes</title>
        <description>&lt;p&gt;&lt;strong&gt;Updates&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2018-04-10&lt;/strong&gt; This post was &lt;a href=&quot;https://news.ycombinator.com/item?id=16790338&quot;&gt;discussed on Hacker News&lt;/a&gt;.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt; &lt;/p&gt;

&lt;p&gt;So far in this series on the mathematics of &lt;a href=&quot;http://gabrielecirulli.github.io/2048&quot;&gt;2048&lt;/a&gt;, we’ve used Markov chains to learn that &lt;a href=&quot;/articles/2017/08/05/markov-chain-2048.html&quot;&gt;it takes at least 938.8 moves&lt;/a&gt; on average to win, and we’ve explored the number of possible board configurations in the game using &lt;a href=&quot;/articles/2017/09/17/counting-states-combinatorics-2048.html&quot;&gt;combinatorics&lt;/a&gt; and then &lt;a href=&quot;/articles/2017/12/10/counting-states-enumeration-2048.html&quot;&gt;exhaustive enumeration&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In this post, we’ll use a mathematical framework called a Markov Decision Process to find provably optimal strategies for 2048 when played on the 2x2 and 3x3 boards, and also on the 4x4 board up to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;64&lt;/code&gt; tile. For example, here is an optimal player for the 2x2 game to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;32&lt;/code&gt; tile:&lt;/p&gt;

&lt;p class=&quot;twenty48-policy-player&quot; data-board-size=&quot;2&quot; data-max-exponent=&quot;5&quot; data-packed-policy-path=&quot;/assets/2048/game-board_size-2.max_exponent-5/layer_model-max_depth-0/packed_policy-discount-1.method-v.alternate_action_tolerance-1e-09.threshold-0.alternate_actions-false.values-false.txt&quot; data-initial-seed=&quot;47&quot; data-arcade-mode=&quot;true&quot;&gt;Loading&amp;hellip;&lt;/p&gt;

&lt;p&gt;The random seed determines the random sequence of tiles that the game adds to the board. The ‘strategy’ the player follows is defined by a table, called a &lt;em&gt;policy&lt;/em&gt;, that tells it which direction it should swipe in every possible board configuration. In this post, we’ll see how to construct a policy that is optimal, in the sense that it maximizes the player’s chances of reaching the target &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;32&lt;/code&gt; tile.&lt;/p&gt;

&lt;p&gt;It turns out that the 2x2 game to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;32&lt;/code&gt; tile is very hard to win — even when playing optimally, the player only wins about 8% of the time, which probably does not make for a very fun game. The 2x2 games are qualitatively quite different to the 4x4 games, but they’ll still be useful to introduce the key ideas.&lt;/p&gt;

&lt;p&gt;Ideally we’d be able to find an optimal policy for the full game on the 4x4 board to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2048&lt;/code&gt; tile, but as we saw in the previous post, the number of possible board configurations is very large. This makes it infeasible to construct a complete optimal policy for the full game, at least with the methods used here.&lt;/p&gt;

&lt;p&gt;We will however be able to find an optimal policy for the shortened 4x4 game to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;64&lt;/code&gt; tile, and fortunately we’ll see that optimal play on the 3x3 boards looks qualitatively similar, in an admittedly hand-wavy way, to some successful strategies for the full game.&lt;/p&gt;

&lt;p&gt;The (research quality) code behind this article is &lt;a href=&quot;https://github.com/jdleesmiller/twenty48&quot;&gt;open source&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;markov-decision-processes-for-2048&quot;&gt;Markov Decision Processes for 2048&lt;/h2&gt;

&lt;p&gt;Markov Decision Processes (&lt;a href=&quot;https://en.wikipedia.org/wiki/Markov_decision_process&quot;&gt;MDPs&lt;/a&gt;) are a mathematical framework for modeling and solving problems in which we need to make a sequence of related decisions in the presence of uncertainty. Such problems are all around us, and MDPs find many &lt;a href=&quot;http://stats.stackexchange.com/questions/145122/real-life-examples-of-markov-decision-processes&quot;&gt;applications&lt;/a&gt; in &lt;a href=&quot;https://en.wikipedia.org/wiki/Decision_theory#Choice_under_uncertainty&quot;&gt;economics&lt;/a&gt;, &lt;a href=&quot;https://www.minet.uni-jena.de/Marie-Curie-ITN/SMIF/talks/Baeuerle.pdf&quot;&gt;finance&lt;/a&gt;, and &lt;a href=&quot;http://incompleteideas.net/book/the-book.html&quot;&gt;artificial intelligence&lt;/a&gt;. For 2048, the sequence of decisions is the direction to swipe in each turn, and the uncertainty arises because the game adds new tiles to the board at random.&lt;/p&gt;

&lt;p&gt;To set up the game of 2048 as an MDP, we will need to write it down in a specific way. This will involve six main concepts: &lt;em&gt;states&lt;/em&gt;, &lt;em&gt;actions&lt;/em&gt; and &lt;em&gt;transition probabilities&lt;/em&gt; will encode the game’s dynamics; &lt;em&gt;rewards&lt;/em&gt;, &lt;em&gt;values&lt;/em&gt; and &lt;em&gt;policies&lt;/em&gt; will be used to capture what the player is trying to accomplish and how they should do so. To develop these six concepts, we will take as an example the smallest non-trivial 2048-like game, which is played on the 2x2 board only up to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;8&lt;/code&gt; tile. Let’s start with the first three.&lt;/p&gt;

&lt;h3 id=&quot;states-actions-and-transition-probabilities&quot;&gt;States, Actions and Transition Probabilities&lt;/h3&gt;

&lt;p&gt;A &lt;em&gt;state&lt;/em&gt; captures the configuration of the board at a given point in the game by specifying the value of the tile, if any, in each of the board’s cells. For example, &lt;img src=&quot;/assets/2048/2x2_s0_1_1_0.svg&quot; style=&quot;height: 2em;&quot; /&gt; is a possible state in a game on a 2x2 board. An &lt;em&gt;action&lt;/em&gt; is swiping left, right, up or down. Each time the player takes an action, the process transitions to a new state.&lt;/p&gt;

&lt;p&gt;The &lt;em&gt;transition probabilities&lt;/em&gt; encode the game’s dynamics by determining which states are likely to come next, in view of the current state and the player’s action. Fortunately, we can find out exactly how 2048 works by reading &lt;a href=&quot;https://github.com/gabrielecirulli/2048&quot;&gt;its source code&lt;/a&gt;. Most important &lt;sup id=&quot;fnref:merging&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:merging&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; is the process the game uses to place a random tile on the board, which is always the same: &lt;em&gt;pick an available cell uniformly at random, then add a new tile either with value &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt;, with probability 0.9, or value &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt;, with probability 0.1&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;At the start of each game, two random tiles are added using this process. For example, one of these possible start states is &lt;img src=&quot;/assets/2048/2x2_s0_1_1_0.svg&quot; style=&quot;height: 2em;&quot; alt=&quot;- 2 2 -; two 2 tiles on the anti-diagonal&quot; /&gt;. For each of the possible actions in this state, namely &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;L&lt;/code&gt;eft, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;R&lt;/code&gt;right, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;U&lt;/code&gt;p and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;D&lt;/code&gt;own, the possible next states and the corresponding transition probabilities are &lt;sup id=&quot;fnref:dot&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:dot&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
&lt;img src=&quot;/assets/2048/mdp_s0_1_1_0_with_no_canonicalization.svg&quot; alt=&quot;Actions and transitions from the state - 2 2 -&quot; style=&quot;max-height: 40em;&quot; /&gt;
&lt;/p&gt;

&lt;p&gt;In this diagram, there is an arrow for each possible transition to a successor state, on the right hand side. The weight of the arrow and the label indicate the corresponding transition probability. For example, if the player swipes right (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;R&lt;/code&gt;), both &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt; tiles go to the right edge, leaving two available cells on the left. The new tile will be a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt; with probability 0.1, and it can either go into the top left or the bottom left cell, so the probability of the state &lt;img src=&quot;/assets/2048/2x2_s2_1_0_1.svg&quot; style=&quot;height: 2em;&quot; alt=&quot;4 2 - 2&quot; /&gt; is \(0.1 \times 0.5 = 0.05\).&lt;/p&gt;

&lt;p&gt;From each of those successor states, we can continue this process of enumerating their allowed actions and successor states, recursively. For &lt;img src=&quot;/assets/2048/2x2_s2_1_0_1.svg&quot; style=&quot;height: 2em;&quot; alt=&quot;4 2 - 2&quot; /&gt;, the possible successors are:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
&lt;img src=&quot;/assets/2048/mdp_s2_1_0_1_with_no_canonicalization.svg&quot; alt=&quot;Actions and transitions from the state 4 2 - 2&quot; style=&quot;max-height: 40em;&quot; /&gt;
&lt;/p&gt;

&lt;p&gt;Here swiping right is not allowed, because none of the tiles can move right. Moreover, if the player reaches the successor state &lt;img src=&quot;/assets/2048/2x2_s2_1_1_2.svg&quot; style=&quot;height: 2em;&quot; alt=&quot;4 2 2 4&quot; /&gt;, highlighted in red, they have lost, because there are no allowed actions from that state. This would happen if the player were to swipe left, and the game were to place a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt; tile, which it would do with probability 0.1; this suggests that swiping left may not be the best action in this state.&lt;/p&gt;

&lt;p&gt;For one final example, if the player moves up instead of left, one of the possible successor states is &lt;img src=&quot;/assets/2048/2x2_s2_2_1_0.svg&quot; style=&quot;height: 2em;&quot; alt=&quot;4 4 2 -&quot; /&gt;, and if we enumerate the allowed actions and and successor states from that state, we can see that swiping left or right will then result in an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;8&lt;/code&gt; tile, which means the game is won (highlighted in green):&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
&lt;img src=&quot;/assets/2048/mdp_s2_2_1_0_with_no_canonicalization.svg&quot; alt=&quot;Actions and transitions from the state 4 4 2 -&quot; style=&quot;max-height: 40em;&quot; /&gt;
&lt;/p&gt;

&lt;p&gt;If we repeat this process for all of the possible start states, and all of their possible successor states, and so on recursively until win or lose states are reached, we can build up a full model with all of the possible states, actions and their transition probabilities. For the 2x2 game to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;8&lt;/code&gt; tile, that model looks like this (&lt;a href=&quot;/assets/2048/mdp_2x2_3_with_no_canonicalization.svg&quot;&gt;click to enlarge&lt;/a&gt;; you may then need to scroll down):&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
&lt;a href=&quot;/assets/2048/mdp_2x2_3_with_no_canonicalization.svg&quot;&gt;&lt;img src=&quot;/assets/2048/mdp_2x2_3_with_no_canonicalization.svg&quot; alt=&quot;Full MDP model for the 2x2 game to the 8 tile without canonicalization techniques from Appendix A&quot; id=&quot;mdp_2x2_3_with_no_canonicalization&quot; /&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;To make the diagram smaller, all of the losing states have been collapsed into a single &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lose&lt;/code&gt; state, shown as a red oval, and all of the winning states have been similarly collapsed into a single &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;win&lt;/code&gt; state, shown as a green star. This is because we don’t particularly care how the player won or lost, only that they did.&lt;/p&gt;

&lt;p&gt;Play proceeds roughly from left to right in the diagram, because the states have been organized into ‘layers’ by the sum of their tiles. A useful property of the game is that after each action the sum of the tiles on the board increases by either 2 or 4. This is because merging tiles does not change the sum of the tiles on the board, and the game always adds either a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt; tile or a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt; tile. The possible start states, which are in the layers with sum 4, 6 and 8, are drawn in blue.&lt;/p&gt;

&lt;p&gt;Even for this smallest example, there are 70 states and 530 transitions in the model. It is possible significantly reduce those numbers, however, by observing that many of the states we’ve enumerated above are trivially related by rotations and reflections, as described in &lt;a href=&quot;#appendix-a-canonicalization&quot;&gt;Appendix A&lt;/a&gt;. This observation is important in practice for reducing the size of the models so that they can be solved efficiently, and it makes for more legible diagrams, but it is not essential for us to move on to our second set of MDP concepts.&lt;/p&gt;

&lt;h3 id=&quot;rewards-values-and-policies&quot;&gt;Rewards, Values and Policies&lt;/h3&gt;

&lt;p&gt;To complete our specification of the model, we need to somehow encode the fact that the player’s objective is to reach the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;win&lt;/code&gt; state &lt;sup id=&quot;fnref:objectives&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:objectives&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;. We do this by defining &lt;em&gt;rewards&lt;/em&gt;. In general, each time an MDP enters a state, the player receives a reward that depends on the state. Here we’ll set the reward for entering the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;win&lt;/code&gt; state to 1, and the reward for entering all other states to 0. That is, the one and only way to earn a reward is to reach the win state. &lt;sup id=&quot;fnref:absorbing&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:absorbing&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;4&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Now that we have an MDP model of the game in terms of states, actions, transition probabilities and rewards, we are ready to solve it. A solution for an MDP is called a &lt;em&gt;policy&lt;/em&gt;. It is basically a table that lists for every possible state which action to take in that state. To solve an MDP is to find an &lt;em&gt;optimal policy&lt;/em&gt;, which is one that allows the player to collect as much reward as possible over time.&lt;/p&gt;

&lt;p&gt;To make this precise, we will need our final MDP concept: the &lt;em&gt;value&lt;/em&gt; of a state according to a given policy is the expected, discounted reward the player will collect if they start from that state and follow the policy thereafter. To explain what that means will require some notation.&lt;/p&gt;

&lt;p&gt;Let \(S\) be the set of states, and for each state \(s \in S\), let \(A_s\) be the set of actions that are allowed in state \(s\). Let \(\Pr(s’ | s, a)\) denote the probability of transitioning to each successor state \(s’ \in S\), given that the process is in state \(s \in S\) and the player takes action \(a \in A_s\). Let \(R(s)\) denote the reward for entering state \(s\). Finally, let \(\pi\) denote a policy and \(\pi(s) \in A_s\) denote the action to take in state \(s\) when following policy \(\pi\).&lt;/p&gt;

&lt;p&gt;For a given policy \(\pi\) and state \(s\), the value of state \(s\) according to \(\pi\) is
\[
V^\pi(s) = R(s) + \gamma \sum_{s’} \Pr(s’ | s, \pi(s)) V^\pi(s’)
\]
where the first term is the immediate reward, and the summation gives the expected value of the successor states, assuming the player continues to follow the policy.&lt;/p&gt;

&lt;p&gt;The factor \(\gamma\) is a &lt;em&gt;discount factor&lt;/em&gt; that trades off the value of the immediate reward against the value of the expected future rewards. In other words, it accounts for &lt;a href=&quot;https://en.wikipedia.org/wiki/Time_value_of_money&quot;&gt;the time value of money&lt;/a&gt;: a reward now is typically worth more than the same reward later. If \(\gamma\) is close to 1, it means that the player is very patient: they don’t mind waiting for future rewards; likewise, smaller values of \(\gamma\) mean that the player is less patient. For now, we’ll set the discount factor \(\gamma\) to 1, which matches our assumption that the player cares only about winning, not about how long it takes to win &lt;sup id=&quot;fnref:discounting&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:discounting&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;5&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;

&lt;p&gt;So, how do we find the policy? For each state, we want to choose the action that maximizes the expected future value:&lt;/p&gt;

&lt;p&gt;\[
\pi(s) = \mathop{\mathrm{argmax}}\limits_{a \in A_s} \left\{
\sum_{s’} \Pr(s’ | s, a) V^\pi(s’)
\right\}
\]&lt;/p&gt;

&lt;p&gt;So, this gives us two linked equations, and we can solve them iteratively. That is, pick an initial policy, which might be very simple, compute the value of every state under that simple policy, and then find a new policy based on that value function, and so on. Perhaps remarkably, under very modest technical conditions, such an iterative process is guaranteed to converge to an optimal policy, \(\pi^*\), and an optimal value function \(V^{\pi^*}\) with respect to that optimal policy.&lt;/p&gt;

&lt;p&gt;This standard iterative approach works well for the MDP models for games on the 2x2 board, but it breaks down for the 3x3 and 4x4 game models, which have many more states and therefore take much more memory and compute power. Fortunately, it turns out that we can exploit the particular structure of our 2048 models to solve these equations much more efficiently, as described in &lt;a href=&quot;#appendix-b-solution-methods&quot;&gt;Appendix B&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;optimal-play-on-the-2x2-board&quot;&gt;Optimal Play on the 2x2 Board&lt;/h2&gt;

&lt;p&gt;We’re now ready to see some optimal policies in action! If you leave the random seed at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;42&lt;/code&gt; and press the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Start&lt;/code&gt; button below, you’ll see it reach the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;8&lt;/code&gt; tile in 5 moves. The random seed determines the sequence of new tiles that the game will place; if you choose a different random seed by clicking the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;⟲&lt;/code&gt; button, you will (usually) see a different game unfold.&lt;/p&gt;

&lt;p class=&quot;twenty48-policy-player&quot; data-board-size=&quot;2&quot; data-max-exponent=&quot;3&quot; data-packed-policy-path=&quot;/assets/2048/game-board_size-2.max_exponent-3/layer_model-max_depth-0/packed_policy-discount-1.method-v.alternate_action_tolerance-1e-09.threshold-0.alternate_actions-false.values-true.txt&quot; data-initial-seed=&quot;42&quot;&gt;Loading&amp;hellip;&lt;/p&gt;

&lt;p&gt;For the 2x2 game to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;8&lt;/code&gt; tile, there is not actually much to see. If the player follows the optimal policy, they will always win. (As we saw above, when building the transition probabilities, if the player does not play optimally, it is possible to lose.) This is reflected in the fact that the value of the state remains at 1.00 for the whole game — when playing optimally, there is at least one action in every reachable state that leads to a win.&lt;/p&gt;

&lt;p&gt;If we instead ask the player to play to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;16&lt;/code&gt; tile, a win is no longer assured even when playing optimally. In this case, picking a new random seed should lead to a win 96% of the time for the game to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;16&lt;/code&gt; tile, so I’ve set the initial seed to one of the rare seeds that leads to a loss.&lt;/p&gt;

&lt;p class=&quot;twenty48-policy-player&quot; data-board-size=&quot;2&quot; data-max-exponent=&quot;4&quot; data-packed-policy-path=&quot;/assets/2048/game-board_size-2.max_exponent-4/layer_model-max_depth-0/packed_policy-discount-1.method-v.alternate_action_tolerance-1e-09.threshold-0.alternate_actions-false.values-true.txt&quot; data-initial-seed=&quot;20&quot;&gt;Loading&amp;hellip;&lt;/p&gt;

&lt;p&gt;As a result of setting the discount factor, \(\gamma\), to 1, the value of each state also conveniently tells us the probability of winning from that state. Here the value starts at 0.96 and then eventually drops to 0.90, because the outcome hinges on the next tile being a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt; tile. Unfortunately, the game delivers a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt; tile, so the player loses, despite playing optimally.&lt;/p&gt;

&lt;p&gt;Finally, we’ve &lt;a href=&quot;/articles/2017/09/17/counting-states-combinatorics-2048.html#layer-reachability&quot;&gt;previously established&lt;/a&gt; that the largest reachable tile on the 2x2 board is the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;32&lt;/code&gt; tile, so let’s see the corresponding optimal policy. Here the probability of winning drops to only 8%. (This is the same game and the same policy used in the introduction, but now it’s interactive.)&lt;/p&gt;

&lt;p class=&quot;twenty48-policy-player&quot; data-board-size=&quot;2&quot; data-max-exponent=&quot;5&quot; data-packed-policy-path=&quot;/assets/2048/game-board_size-2.max_exponent-5/layer_model-max_depth-0/packed_policy-discount-1.method-v.alternate_action_tolerance-1e-09.threshold-0.alternate_actions-false.values-true.txt&quot; data-initial-seed=&quot;47&quot;&gt;Loading&amp;hellip;&lt;/p&gt;

&lt;p&gt;It’s worth remarking that each of the policies above is &lt;em&gt;an&lt;/em&gt; optimal policy for the corresponding game, but there is no guarantee of uniqueness. There may be many optimal policies that are equivalent, but we can say with certainty that none of them are strictly better.&lt;/p&gt;

&lt;p&gt;If you’d like to explore these models for the 2x2 game in more depth, &lt;a href=&quot;#appendix-a-canonicalization&quot;&gt;Appendix A&lt;/a&gt; provides some diagrams that show all the possible paths through the game.&lt;/p&gt;

&lt;h1 id=&quot;optimal-play-on-the-3x3-board&quot;&gt;Optimal Play on the 3x3 Board&lt;/h1&gt;

&lt;p&gt;On the 3x3 board, it is possible to play up to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1024&lt;/code&gt; tile, and that game has some &lt;a href=&quot;/articles/2017/12/10/counting-states-enumeration-2048.html#results&quot;&gt;25 million states&lt;/a&gt;. Drawing an MDP diagram like we did for the 2x2 games is therefore clearly out of the question, but we can still watch an optimal policy in action &lt;sup id=&quot;fnref:missing&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:missing&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;6&lt;/a&gt;&lt;/sup&gt;:&lt;/p&gt;

&lt;p class=&quot;twenty48-policy-player&quot; data-board-size=&quot;3&quot; data-max-exponent=&quot;10&quot; data-packed-policy-path=&quot;/assets/2048/game-board_size-3.max_exponent-a/layer_model-max_depth-0/packed_policy-discount-1.method-v.alternate_action_tolerance-1e-09.threshold-1e-07.alternate_actions-false.values-true.txt&quot; data-initial-seed=&quot;56&quot;&gt;Loading&amp;hellip;&lt;/p&gt;

&lt;p&gt;Much like the 2x2 game to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;32&lt;/code&gt;, the 3x3 game to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1024&lt;/code&gt; is very hard to win — if playing optimally, the probability of winning is only about 1%. For some less frustrating entertainment, here also is the game to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;512&lt;/code&gt;, for which the probability of winning if playing optimally is much higher, at about 74%:&lt;/p&gt;

&lt;p class=&quot;twenty48-policy-player&quot; data-board-size=&quot;3&quot; data-max-exponent=&quot;9&quot; data-packed-policy-path=&quot;/assets/2048/game-board_size-3.max_exponent-9/layer_model-max_depth-0/packed_policy-discount-1.method-v.alternate_action_tolerance-1e-09.threshold-1e-07.alternate_actions-false.values-true.txt&quot; data-initial-seed=&quot;42&quot;&gt;Loading&amp;hellip;&lt;/p&gt;

&lt;p&gt;At the risk of anthropomorphizing a large table of states and actions, which is what a policy is, I see here elements of strategies that I use when I play 2048 on the 4x4 board &lt;sup id=&quot;fnref:me&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:me&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;7&lt;/a&gt;&lt;/sup&gt;. We can see the policy pinning the high value tiles to the edges and usually corners (though in the game to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1024&lt;/code&gt;, it often puts the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;512&lt;/code&gt; tile in the middle of an edge). We can also see it being ‘lazy’ — even when it has two high value tiles lined up to merge, it will continue merging lower value tiles. Particularly within the tight constraints of the 3x3 board, it makes sense that it will take the opportunity to &lt;a href=&quot;/articles/2017/08/05/markov-chain-2048.html#binomial-probabilities&quot;&gt;increase the sum of its tiles&lt;/a&gt; at no risk of (immediately) losing — if it gets stuck merging smaller tiles, it can always merge the larger ones, which opens up the board.&lt;/p&gt;

&lt;p&gt;It’s important to note that we did not teach the policy about these strategies or give it any other hints about how to play. All of its behaviors and any apparent intelligence emerges solely from solving an optimization problem with respect to the transition probabilities and reward function we supplied.&lt;/p&gt;

&lt;h1 id=&quot;optimal-play-on-the-4x4-board&quot;&gt;Optimal Play on the 4x4 Board&lt;/h1&gt;

&lt;p&gt;As established in the last post, the game to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2048&lt;/code&gt; tile on the 4x4 board has at least trillions of states, and so far it has not been possible to even enumerate all the states, let alone solve the resulting MDP for an optimal policy.&lt;/p&gt;

&lt;p&gt;We can, however, complete the enumeration and the solve for the 4x4 game up to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;64&lt;/code&gt; tile — that model has “only” about 40 billion states. Like the 2x2 game to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;8&lt;/code&gt; above, it is impossible to lose when playing optimally. This does not make for very interesting viewing, because in many cases there are several good actions, and the choice between them is arbitrary.&lt;/p&gt;

&lt;p&gt;However, if we reduce the discount factor, \(\gamma\), that makes the player slightly impatient, so that they prefer to win sooner rather than later. It then looks a bit more directed. Here is an optimal player for \(\gamma = 0.99\):&lt;/p&gt;

&lt;p class=&quot;twenty48-policy-player&quot; data-board-size=&quot;4&quot; data-max-exponent=&quot;6&quot; data-packed-policy-path=&quot;/assets/2048/game-board_size-4.max_exponent-6/layer_model-max_depth-0/packed_policy-discount-0.99.method-v.alternate_action_tolerance-1e-09.threshold-1e-07.alternate_actions-false.values-true.txt&quot; data-initial-seed=&quot;42&quot;&gt;Loading&amp;hellip;&lt;/p&gt;

&lt;p&gt;The value starts around 0.72; the exact initial value reflects the expected number of moves it will take to win from the randomly selected start state. It gradually increases with each move, as the reward for reaching the win state gets closer. Again the policy shows good use of the edges and corners to build sequences of tiles in an order that’s convenient to merge.&lt;/p&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;We’ve seen how to represent the game of 2048 as a Markov Decision Process and obtained provably optimal policies for the smaller games on the 2x2 and 3x3 boards and a partial game on the 4x4 board.&lt;/p&gt;

&lt;p&gt;The methods used here require us to enumerate all of the states in the model in order to solve it. Using &lt;a href=&quot;/articles/2017/12/10/counting-states-enumeration-2048.html&quot;&gt;efficient strategies for enumerating the states&lt;/a&gt; and &lt;a href=&quot;#appendix-b-solution-methods&quot;&gt;efficient strategies for solving the model&lt;/a&gt; makes this feasible for models with up to 40 billion states, which was the number for the 4x4 game to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;64&lt;/code&gt;. The calculations for that model took roughly one week on an OVH HG-120 instance with 32 cores at 3.1GHz and 120GB RAM. The next-largest 4x4 game, played up to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;128&lt;/code&gt; tile, is likely to contain many times that number of states and would require many times the computing power. Calculating a provably optimal policy for the full game to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2048&lt;/code&gt; tile will likely require different methods.&lt;/p&gt;

&lt;p&gt;It is common to find that MDPs are too large to solve in practice, so there are a range of proven techniques for finding approximate solutions. These typically involve storing the value function and/or policy approximately, for example by training a (possibly deep) neural network. They can also be trained on simulation data, rather than requiring enumeration of the full state space, using reinforcement learning methods. The availability of provably optimal policies for smaller games may make 2048 a useful test bed for such methods — that would be an interesting future research topic.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt; &lt;/p&gt;

&lt;h2 id=&quot;appendix-a-canonicalization&quot;&gt;Appendix A: Canonicalization&lt;/h2&gt;

&lt;p&gt;As we’ve seen with the full model for the 2x2 game to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;8&lt;/code&gt; tile, the number of states and transitions grows quickly, and even games on the 2x2 board become hard to draw in this form.&lt;/p&gt;

&lt;p&gt;To help keep the size of the model under control, we can reuse an observation from the &lt;a href=&quot;/articles/2017/12/10/counting-states-enumeration-2048.html#canonicalization-and-symmetry&quot;&gt;previous post about enumerating states&lt;/a&gt;: many of the successor states are just rotations or reflections of each other. For example, the states&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
  &lt;img src=&quot;/assets/2048/2x2_s2_1_0_1.svg&quot; alt=&quot;4 2 - 2&quot; /&gt;
  and
  &lt;img src=&quot;/assets/2048/2x2_s1_2_1_0.svg&quot; alt=&quot;2 4 2 -&quot; /&gt;
&lt;/p&gt;
&lt;p&gt;are just mirror images — they are reflections through the vertical axis. If the best action in the first state was to swipe left, the best action in the second state would necessarily be to swipe right. So, from the perspective of deciding which action to take, it suffices to pick one of the states as the &lt;em&gt;canonical state&lt;/em&gt; and determine the best action to take from the canonical state. A state’s canonical state is obtained by finding all of its possible rotations and reflections, writing each one as a number in base 12, and picking the state with the smallest number — see the &lt;a href=&quot;/articles/2017/12/10/counting-states-enumeration-2048.html#canonicalization-and-symmetry&quot;&gt;previous post&lt;/a&gt; for the details. The important point here is that each canonical state stands in for a class of equivalent states that are related to it by rotation and reflection, so we don’t have to deal with them all individually. By replacing the successor states above with their canonical states, we obtain a much more compact diagram:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
&lt;img src=&quot;/assets/2048/mdp_s0_1_1_0_with_state_canonicalization.svg&quot; alt=&quot;Actions and transitions from the state - 2 2 - with successor state canonicalization&quot; /&gt;
&lt;/p&gt;

&lt;p&gt;It’s somewhat unfortunate that the diagram appears to imply that swiping up (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;U&lt;/code&gt;) from &lt;img src=&quot;/assets/2048/2x2_s0_1_1_0.svg&quot; style=&quot;height: 2em;&quot; /&gt; somehow leads to &lt;img src=&quot;/assets/2048/2x2_s0_1_1_1.svg&quot; style=&quot;height: 2em;&quot; /&gt;. However, the paradox is resolved if you read the arrows to also include a rotation or reflection as required to find the actual successor’s canonical state.&lt;/p&gt;

&lt;h3 id=&quot;equivalent-actions&quot;&gt;Equivalent Actions&lt;/h3&gt;

&lt;p&gt;We can also observe that in the diagram above it does not actually matter which direction the player swipes from the state &lt;img src=&quot;/assets/2048/2x2_s0_1_1_0.svg&quot; style=&quot;height: 2em;&quot; /&gt; — the canonical successor states and their transition probabilities are the same for all of the actions. To see why, it helps to look at the ‘intermediate state’ after the player has swiped to move the tiles but before the game has added a new random tile:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
&lt;img src=&quot;/assets/2048/mdp_s0_1_1_0_move_states.svg&quot; alt=&quot;Intermediate states resulting from moving left, right, up or down from the state - 2 2 -&quot; /&gt;
&lt;/p&gt;

&lt;p&gt;The intermediate states are drawn with dashed lines. The key observation is that they are all related by 90° rotations. These rotations don’t matter when we eventually canonicalize the successor states.&lt;/p&gt;

&lt;p&gt;More generally, if two or more actions have the same canonical ‘intermediate state’, then those actions must have identical canonical successor states and transition probabilities and therefore are equivalent. In the example above, the canonical intermediate state happens to be the last one, &lt;img src=&quot;/assets/2048/2x2_s0_0_1_1.svg&quot; style=&quot;height: 2em;&quot; alt=&quot;- - 2 2&quot; /&gt;.&lt;/p&gt;

&lt;p&gt;We can therefore simplify the diagram for &lt;img src=&quot;/assets/2048/2x2_s0_1_1_0.svg&quot; style=&quot;height: 2em;&quot; /&gt; further if we just collapse all of the equivalent actions together:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
&lt;img src=&quot;/assets/2048/mdp_s0_1_1_0_with_action_canonicalization.svg&quot; alt=&quot;Actions and transitions from the state - 2 2 - with successor state and action canonicalization&quot; /&gt;
&lt;/p&gt;

&lt;p&gt;Of course, states for which all actions are equivalent in this way are relatively rare. Considering another potential start state, &lt;img src=&quot;/assets/2048/2x2_s0_0_1_1.svg&quot; style=&quot;height: 2em;&quot; /&gt;, we see that swiping left and right are equivalent, but swiping up is distinct; swiping down is not allowed, because the tiles are already on the bottom.&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
&lt;img src=&quot;/assets/2048/mdp_s0_0_1_1_with_action_canonicalization.svg&quot; alt=&quot;Actions and transitions from the state - - 2 2 with successor state and action canonicalization&quot; /&gt;
&lt;/p&gt;

&lt;h3 id=&quot;mdp-model-diagrams-for-2x2-games&quot;&gt;MDP Model Diagrams for 2x2 Games&lt;/h3&gt;

&lt;p&gt;Using canonicalization, we can shrink the models enough to just about draw out the full MDPs for some small games. Let’s again start with the smallest non-trivial model: the 2x2 game played just up to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;8&lt;/code&gt; tile (&lt;a href=&quot;/assets/2048/mdp_2x2_3.svg&quot;&gt;click to enlarge&lt;/a&gt;):&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
&lt;a href=&quot;/assets/2048/mdp_2x2_3.svg&quot;&gt;&lt;img src=&quot;/assets/2048/mdp_2x2_3.svg&quot; alt=&quot;Full MDP model for the 2x2 game up to the 8 tile&quot; /&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;Compared to the figure without canonicalization, this is much more compact. We can see that the shortest possible game comprises only a single move: if we are lucky enough to start in the state &lt;img src=&quot;/assets/2048/2x2_s0_0_2_2.svg&quot; style=&quot;height: 2em;&quot; /&gt; with two adjacent &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt; tiles, which happens in only one game in 150, we just need to merge them together to reach an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;8&lt;/code&gt; tile and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;win&lt;/code&gt; state. On the other hand, we can see that it is still possible to lose: if from state &lt;img src=&quot;/assets/2048/2x2_s0_0_2_2.svg&quot; style=&quot;height: 2em;&quot; /&gt; the player swipes up, they reach &lt;img src=&quot;/assets/2048/2x2_s0_1_2_2.svg&quot; style=&quot;height: 2em;&quot; /&gt; with probability 0.9, and then if they swipe up again, they reach &lt;img src=&quot;/assets/2048/2x2_s2_1_1_2.svg&quot; style=&quot;height: 2em;&quot; /&gt; with probability 0.9, at which point the game is lost.&lt;/p&gt;

&lt;p&gt;We can further simplify the diagram if we know the optimal policy. Specifying the policy induces a Markov chain from the MDP model, because every state has a single action, or group of equivalent actions, identified by the policy. The induced chain for the 2x2 game to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;8&lt;/code&gt; tile is:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
&lt;a href=&quot;/assets/2048/mdp_2x2_3_optimal.svg&quot;&gt;&lt;img src=&quot;/assets/2048/mdp_2x2_3_optimal.svg&quot; alt=&quot;MDP model with only the optimal actions for the 2x2 game up to the 8 tile&quot; /&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;We can see that the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lose&lt;/code&gt; state no longer has any edges leading into it, because it is impossible to lose when playing optimally in the 2x2 game to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;8&lt;/code&gt; tile. Each state is also now labelled with its value, which in this case is always 1.000. Because we’ve set the discount factor \(\gamma\) to 1, the value of a state is in fact the probability of winning from that state when playing optimally.&lt;/p&gt;

&lt;p&gt;They get a bit messier, but can build similar models for the 2x2 game to &lt;a href=&quot;/assets/2048/mdp_2x2_4.svg&quot;&gt;the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;16&lt;/code&gt; tile&lt;/a&gt;:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
&lt;a href=&quot;/assets/2048/mdp_2x2_4.svg&quot;&gt;&lt;img src=&quot;/assets/2048/mdp_2x2_4.svg&quot; alt=&quot;Full MDP model for the 2x2 game up to the 16 tile&quot; /&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;If we look at the optimal policy for the game to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;16&lt;/code&gt; tile, we see that the start states (in blue) all have values less than one, and that there are paths to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lose&lt;/code&gt; state, in particular from two states that have tile sum 14:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
&lt;a href=&quot;/assets/2048/mdp_2x2_4_optimal.svg&quot;&gt;&lt;img src=&quot;/assets/2048/mdp_2x2_4_optimal.svg&quot; alt=&quot;MDP model with only the optimal actions for the 2x2 game up to the 16 tile&quot; /&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;That is, even if we play this game to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;16&lt;/code&gt; tile optimally, we can still lose, depending on the particular sequence of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt; tiles that the game deals us. In most cases, we will win, however — the values of the start states are all around 0.96, so we’d expect to win roughly 96 games out of a hundred.&lt;/p&gt;

&lt;p&gt;Our prospects in the game to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;32&lt;/code&gt; tile on the 2x2 board, however, are much worse. Here is the full model:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
&lt;a href=&quot;/assets/2048/mdp_2x2_5.svg&quot;&gt;&lt;img src=&quot;/assets/2048/mdp_2x2_5.svg&quot; alt=&quot;Full MDP model for the 2x2 game up to the 32 tile&quot; /&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;We can see a lot of edges leading to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lose&lt;/code&gt; state, which is a bad sign. This is confirmed when we look at the diagram restricted to optimal play:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
&lt;a href=&quot;/assets/2048/mdp_2x2_5_optimal.svg&quot;&gt;&lt;img src=&quot;/assets/2048/mdp_2x2_5_optimal.svg&quot; alt=&quot;MDP model with only the optimal actions for the 2x2 game up to the 32 tile&quot; /&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;The average start state value is around 0.08, so we’d expect to win only about 8 games out of a hundred. The main reason becomes clear if we look at the right hand edge of the chain: once we reach a state with tile sum 28, the only way to win is to get a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt; tile in order to reach the state &lt;img src=&quot;/assets/2048/2x2_s2_2_3_4.svg&quot; style=&quot;height: 2em;&quot; /&gt;. If we get a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt; tile, which happens 90% of the time, we lose. It’s probably not a very fun game.&lt;/p&gt;

&lt;h2 id=&quot;appendix-b-solution-methods&quot;&gt;Appendix B: Solution Methods&lt;/h2&gt;

&lt;p&gt;To efficiently solve an MDP model like the ones we’ve constructed here for 2048, we can exploit several important properties of its structure:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;The transition model is a &lt;a href=&quot;https://en.wikipedia.org/wiki/Directed_acyclic_graph&quot;&gt;directed acyclic graph&lt;/a&gt; (DAG). The sum of the tiles must increase, namely by either 2 or 4, with each move, so it is never possible to go back to a state you have already visited.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Moreover, the states can be organized into ‘layers’ by the sums of their tiles, as we did in &lt;a href=&quot;#mdp_2x2_3_with_no_canonicalization&quot;&gt;the first MDP model figure&lt;/a&gt;, and all transitions will be from the current layer with sum \(s\) to either the next layer, with sum \(s+2\), or the one after, with sum \(s+4\).&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;All states in the layer with the largest sum will transition to either a lose or win state, which have a known value, namely 0 or 1, respectively.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Property (3) means that we can loop through all of the states in the last layer, in which all successor values are known, to generate the value function for that last layer. Then, using property (2), we know that the states in the second last layer must transition to either states in the last layer, for which we have just calculated values, or to a win or lose state, which have known values. In this way we can work backward, layer by layer, always knowing the values of states in the next two layers; this allows us to build both the value function and the optimal policy for the current layer. In general, this approach to solving MDPs is called &lt;a href=&quot;https://en.wikipedia.org/wiki/Backward_induction&quot;&gt;backward induction&lt;/a&gt;, which is a particular type of &lt;a href=&quot;https://en.wikipedia.org/wiki/Markov_decision_process#Algorithms&quot;&gt;dynamic programming&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In the &lt;a href=&quot;/articles/2017/12/10/counting-states-enumeration-2048.html#appendix-b-layers-and-mapreduce-for-parallelism&quot;&gt;previous post&lt;/a&gt; we worked forward from the start states to enumerate all of the states, layer by layer, using a map-reduce approach to parallelize the work within each layer. For the solve, we can use the output of that enumeration, which is a large list of states, to work backward, again using a map-reduce approach to parallelize the work within each layer. And like last time we can further break up the layers into ‘parts’ by their maximum tile value, with some additional book keeping.&lt;/p&gt;

&lt;p&gt;The &lt;a href=&quot;https://github.com/jdleesmiller/twenty48/blob/3605cfaeba0a602d9917f84d1a2862afe4ad1bb6/ext/twenty48/layer_solver.hpp&quot;&gt;main solver implementation&lt;/a&gt; is still fairly memory-intensive, because it has to keep the value functions for up to four parts in memory at once in order to process a given part in one pass. This can be reduced to one part in memory at a time if we calculate what is usually called \(Q^\pi(s,a)\), the value for each possible state-action pair according to policy \(\pi\), rather than \(V^\pi(s)\), but &lt;a href=&quot;https://github.com/jdleesmiller/twenty48/blob/3605cfaeba0a602d9917f84d1a2862afe4ad1bb6/ext/twenty48/layer_q_solver.hpp&quot;&gt;that solver implementation&lt;/a&gt; proved to be much slower, so all of the results presented here use the main \(V^\pi\) solver on a machine with lots of RAM.&lt;/p&gt;

&lt;p&gt;With both solvers, the layered structure of the model allows us to build and solve a model in just one forward pass and one backward pass, which is a substantial improvement on the usual iterative solution method, and one of the reasons that we’re able to solve these fairly large MDP models with billions of states. The canonicalization methods in &lt;a href=&quot;#appendix-a-canonicalization&quot;&gt;Appendix A&lt;/a&gt;, which reduce the number of states we need to consider, and the &lt;a href=&quot;/articles/2017/12/10/counting-states-enumeration-2048.html#appendix-a-bit-bashing-for-efficiency&quot;&gt;low level efficiency gains&lt;/a&gt; from the previous post are also important reasons.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt; &lt;/p&gt;

&lt;p&gt;Thanks to &lt;a href=&quot;https://twitter.com/h0peth0mas&quot;&gt;Hope Thomas&lt;/a&gt; for reviewing drafts of this article.&lt;/p&gt;

&lt;p&gt;If you’ve read this far, perhaps you should &lt;a href=&quot;https://twitter.com/jdleesmiller&quot;&gt;follow me on twitter&lt;/a&gt;, or even apply to work at &lt;a href=&quot;https://www.overleaf.com/jobs&quot;&gt;Overleaf&lt;/a&gt;. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:)&lt;/code&gt;&lt;/p&gt;

&lt;h1 id=&quot;footnotes&quot;&gt;Footnotes&lt;/h1&gt;

&lt;script src=&quot;/assets/2048/mdp_player.6a864e01fc49fb07563a.js&quot; type=&quot;text/javascript&quot; charset=&quot;utf-8&quot;&gt;&lt;/script&gt;

&lt;script type=&quot;text/x-mathjax-config&quot;&gt;
MathJax.Hub.Config({
  TeX: { equationNumbers: { autoNumber: &quot;AMS&quot; } }
});
&lt;/script&gt;

&lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML&quot; type=&quot;text/javascript&quot;&gt;&lt;/script&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:merging&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;There is also some nuance in how tiles are merged: if you have four &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt; tiles in a row, for example, and you swipe to merge them, the result is two &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt; tiles, not a single &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;8&lt;/code&gt; tile. That is, you can’t merge newly merged tiles in a single swipe. The original code for merging tiles &lt;a href=&quot;https://github.com/gabrielecirulli/2048/blob/ac03b1f01628038039b74b67f2e284b233bd143e/js/game_manager.js#L145-L180&quot;&gt;is here&lt;/a&gt;, and the simplified but equivalent code I used to merge a line (row or column) of tiles &lt;a href=&quot;https://github.com/jdleesmiller/twenty48/blob/master/ext/twenty48/line.hpp#L29-L54&quot;&gt;is here&lt;/a&gt; with &lt;a href=&quot;https://github.com/jdleesmiller/twenty48/blob/3605cfaeba0a602d9917f84d1a2862afe4ad1bb6/test/twenty48/common/line_with_known_tests.rb&quot;&gt;tests here&lt;/a&gt;. &lt;a href=&quot;#fnref:merging&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:dot&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;The graph diagrams here come from the excellent &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dot&lt;/code&gt; tool in &lt;a href=&quot;http://www.graphviz.org/&quot;&gt;graphviz&lt;/a&gt;. &lt;a href=&quot;#fnref:dot&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:objectives&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;There are several other possible objectives. For example, in the &lt;a href=&quot;/articles/2017/08/05/markov-chain-2048.html&quot;&gt;first post&lt;/a&gt; in this series, I tried to reach the target &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2048&lt;/code&gt; tile in the smallest possible number of moves; and many people I’ve talked to play to reach the largest possible tile, which is also what the game’s points system encourages. These different objectives could also be captured by setting up the model and its rewards appropriately. For example, a simple reward of 1 per move until the player loses would represent the objective of playing as long as possible, which would I think be equivalent to trying to reach the largest possible tile. &lt;a href=&quot;#fnref:objectives&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:absorbing&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;Technically, we need one more special state, in addition to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;win&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lose&lt;/code&gt; states, to make this reward system work as described. The equations that we develop for \(\pi\) and \(V^\pi\) assume that all states have at least one allowed action and successor state, so we can’t just stop the process at the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lose&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;win&lt;/code&gt; states. Instead, we can add an &lt;em&gt;absorbing state&lt;/em&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;end&lt;/code&gt;, with a trivial action that just brings the process back into the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;end&lt;/code&gt; state with probability one. Then we can add a trivial action to both the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lose&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;win&lt;/code&gt; states to transition to the absorbing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;end&lt;/code&gt; state with probability 1. So long as the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;end&lt;/code&gt; state attracts zero reward, it will not change the outcome. It’s also worth mentioning that there are more general ways of defining an MDP that would provide other ways of working around this technicality, for example by making the rewards depend on the whole transition rather than just the state and by making policies stochastic which, as a side effect, means that we can handle states with no allowed actions, but they require more notation. &lt;a href=&quot;#fnref:absorbing&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:discounting&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;In addition to being &lt;a href=&quot;https://en.wikipedia.org/wiki/Time_preference&quot;&gt;well founded&lt;/a&gt; in economic theory, the discount factor is often required technically in order to ensure that the value function converges. If the process runs forever and continues to accumulate additive rewards, it could accumulate infinite value. The geometric discounting ensures that the infinite sum still converges even if this happens. For the processes with the reward structure we’re considering here, we can safely set the discount factor to 1, because the process is constructed so that there are no loops with nonzero reward, and therefore all rewards are bounded. &lt;a href=&quot;#fnref:discounting&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:missing&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;There is a caveat for the 3x3 and 4x4 policies: the full optimal policies for every state are too large to ship to the browser (without unduly imposing on GitHub’s generosity in hosting this website). The player therefore only has access to the policy for the states that have a probability of at least \(10^{-7}\) of actually occurring when playing according to the optimal policy. This means that, unfortunately, roughly one in hundred readers will choose a random seed that takes the process to a state that is not included in the data available on the client, in which case it will stop with an error. These states are selected by calculating the transient probabilities for the absorbing Markov chain induced by the optimal policy. The mathematics are essentially the same as those in &lt;a href=&quot;/articles/2017/08/05/markov-chain-2048.html&quot;&gt;the first post about Markov chains for 2048&lt;/a&gt;. &lt;a href=&quot;#fnref:missing&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:me&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;Of course, I’m not claiming here to be great at 2048. The &lt;a href=&quot;/articles/2017/08/05/markov-chain-2048.html#putting-theory-to-the-test&quot;&gt;data in my first post&lt;/a&gt; suggest otherwise! &lt;a href=&quot;#fnref:me&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</description>
        <pubDate>Sun, 18 Mar 2018 23:00:00 +0000</pubDate>
        <link>http://jdlm.info/articles/2018/03/18/markov-decision-process-2048.html</link>
        <guid isPermaLink="true">http://jdlm.info/articles/2018/03/18/markov-decision-process-2048.html</guid>
        
        
        <category>articles</category>
        
      </item>
    
      <item>
        <title>The Mathematics of 2048: Counting States by Exhaustive Enumeration</title>
        <description>&lt;p&gt;&lt;strong&gt;Updates&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2017-12-11&lt;/strong&gt; This post was &lt;a href=&quot;https://news.ycombinator.com/item?id=15894126&quot;&gt;discussed on Hacker News&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post is the third in a series. Next: &lt;a href=&quot;/articles/2018/03/18/markov-decision-process-2048.html&quot;&gt;Optimal Play with Markov Decision Processes&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt; &lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/2048/2048_improbable.png&quot; alt=&quot;Screenshot of 2048 with an improbable but reachable board configuration&quot; style=&quot;width: 40%; float: right; margin-left: 10pt; border: 6pt solid #eee;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;So far in this series on the mathematics of &lt;a href=&quot;http://gabrielecirulli.github.io/2048&quot;&gt;2048&lt;/a&gt;, we’ve seen that &lt;a href=&quot;/articles/2017/08/05/markov-chain-2048.html&quot;&gt;it takes at least 938.8 moves&lt;/a&gt; on average to win, and we’ve &lt;a href=&quot;/articles/2017/09/17/counting-states-combinatorics-2048.html&quot;&gt;obtained some rough estimates&lt;/a&gt; on the number of possible states using combinatorics.&lt;/p&gt;

&lt;p&gt;In this post, we will try to refine those estimates by simply counting every reachable state by brute force enumeration. There are many states, so this will require some computer science as well as mathematics. With efficient processing and storage of states, we’ll see that it is possible to enumerate all reachable states for 2x2 and 3x3 boards.&lt;/p&gt;

&lt;p&gt;Enumeration of all states for the full game on a 4x4 board remains an open problem. I ran the code on an OVH HG-120 instance with 32 cores at 3.1GHz and 120GB RAM for one month, during which it enumerated 1.3 trillion states, which is a respectable half a million states per second, but that turns out to be nowhere near enough. At present, the best we we’ll be able to do on the 4x4 board is to enumerate all states for the game played up to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;64&lt;/code&gt; tile.&lt;/p&gt;

&lt;p&gt;Overall, the results show that the combinatorial estimates from the last post were substantial overestimates, as suspected. However, the state space for the full game is still very large. If a large nation state or tech company decided that 2048 was their top priority, which may not be the craziest thing that’s happened this year, I think they could probably finish the job.&lt;/p&gt;

&lt;p&gt;The (research quality) code behind this article is &lt;a href=&quot;https://github.com/jdleesmiller/twenty48#3-number-of-states-by-exhaustive-enumeration&quot;&gt;open source&lt;/a&gt;, mainly in ruby and C++.&lt;/p&gt;

&lt;h1 id=&quot;counting-states&quot;&gt;Counting States&lt;/h1&gt;

&lt;p&gt;Here a &lt;em&gt;state&lt;/em&gt; captures a complete configuration of the board by specifying the value of the tile, if any, in each of the board’s cells. Our overall goal is to count all of the states that can actually occur in the game and no more. In the previous post, the estimates from (very basic) combinatorics counted many states that can’t actually occur in the game. By enumerating states systematically from each of the possible start states, we ensure that the states we count are actually reachable in play.&lt;/p&gt;

&lt;p&gt;We also have some freedom in choosing which states are interesting enough to count. Because the game ends when we obtain a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2048&lt;/code&gt; tile, we won’t care about where that tile is or what else is on the board, so we can condense all of the states with a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2048&lt;/code&gt; tile into a special “win” state. Similarly, if we lose, we won’t care exactly how we lost; we can condense all of losing states into a special “lose” state.&lt;/p&gt;

&lt;h1 id=&quot;canonicalization-and-symmetry&quot;&gt;Canonicalization and Symmetry&lt;/h1&gt;

&lt;p&gt;Many other non-interesting states that we’d like to avoid counting arise from the fact that some states are trivially related to each other by rotation or reflection. For example, in the game on a 2x2 board, the states&lt;/p&gt;
&lt;p align=&quot;center&quot;&gt;
  &lt;img src=&quot;/assets/2048/2x2_s2_3_1_0.svg&quot; alt=&quot;4 8 2 -&quot; /&gt;
  and
  &lt;img src=&quot;/assets/2048/2x2_s3_2_0_1.svg&quot; alt=&quot;8 4 - 2&quot; /&gt;
&lt;/p&gt;
&lt;p&gt;are just mirror images — they are reflections through the vertical axis. If we swiped left in the first state, it would be essentially equivalent to swiping right in the second. We can therefore reduce the number of states we have to worry about by treating these states as equivalent. In general, the number of such equivalent states is the number of elements in the &lt;a href=&quot;https://en.wikipedia.org/wiki/Dihedral_group&quot;&gt;dihedral group&lt;/a&gt; for the square, \(D_4\), which is 8. For the first state above, these eight states are:&lt;/p&gt;
&lt;p align=&quot;center&quot;&gt;&lt;img src=&quot;/assets/2048/2x2_canonical.svg&quot; alt=&quot;Eight states with the same canonical state as 4 8 2 -&quot; /&gt;&lt;/p&gt;
&lt;p&gt;In this diagram, which is called a cycle graph, ⤴ denotes a rotation counterclockwise by 90° and ↔ denotes a reflection about the vertical axis. The three states at the top are obtained by one or more rotations; for example, ⤴² means two rotations of 90°, which add up to a rotation of 180°. The four states at the bottom are obtained by zero or more rotations followed by a reflection.&lt;/p&gt;

&lt;p&gt;When the tiles in a state are arranged in a symmetrical pattern, the number of equivalent states may be less than 8, because the symmetry means that some of the states in the above diagram will be identical. For example, the state &lt;img src=&quot;/assets/2048/2x2_s2_1_1_0.svg&quot; style=&quot;height: 2em;&quot; /&gt; has only four, because it is symmetric along the diagonal. However, particularly on 3x3 and 4x4 boards, states with symmetric arrangements of tiles are not so common, so overall we can expect to reduce the number of states we need to count by roughly a factor of 8 by choosing only one of these equivalent states as the &lt;em&gt;canonical&lt;/em&gt; state.&lt;/p&gt;

&lt;h1 id=&quot;states-as-numbers&quot;&gt;States as Numbers&lt;/h1&gt;

&lt;p&gt;Which of the equivalent states should we choose as the canonical state? To answer this question, it will be helpful to think about states as numbers. We’ll also see that this has significant computational benefits in the appendices.&lt;/p&gt;

&lt;p&gt;To write a state as a number, we can start in the top left and read the cells by rows; for each cell, we write a \(0\) digit if the cell is empty, and the digit \(i\) if the cell contains the \(2^i\) tile. For example, on a 2x2 board, the state &lt;img src=&quot;/assets/2048/2x2_s2_3_1_0.svg&quot; alt=&quot;4 8 2 -&quot; style=&quot;height: 2em;&quot; /&gt;
would be written as the number \(2310\), because \(4 = 2^2\), \(8 = 2^3\), \(2 = 2^1\), and the last cell is empty.&lt;/p&gt;

&lt;p&gt;Since we’re only interested in numbers up to 2048, which is \(2^{11}\), we could in principle write any state as a number in base 12 (rather than base 10, as we are accustomed to). However, because computers run on binary numbers, it will be more convenient to think of each state as a number in base 16 — that is, hexadecimal. The state above would therefore be more properly written as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0x2310&lt;/code&gt; for computer scientists or \(2310_{16}\) for mathematicians.&lt;/p&gt;

&lt;p&gt;To find the canonical state, we simply try all eight possible rotations and reflections, convert the resulting states to numbers, and then pick the smallest one. For our example state, &lt;img src=&quot;/assets/2048/2x2_s2_3_1_0.svg&quot; alt=&quot;4 8 2 -&quot; style=&quot;height: 2em;&quot; /&gt;, the candidates and their corresponding numbers are:&lt;/p&gt;

&lt;table style=&quot;width: 1%; margin: 0px auto;&quot;&gt;
&lt;thead&gt;&lt;tr&gt;&lt;th&gt;State&lt;/th&gt;&lt;th&gt;Number&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;img src=&quot;/assets/2048/2x2_s2_3_1_0.svg&quot; alt=&quot;4 8 2 -&quot; style=&quot;height: 2em;&quot; /&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;0x2310&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;img src=&quot;/assets/2048/2x2_s3_2_0_1.svg&quot; alt=&quot;8 4 - 2&quot; style=&quot;height: 2em;&quot; /&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;0x3201&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;img src=&quot;/assets/2048/2x2_s1_0_2_3.svg&quot; alt=&quot;2 - 4 8&quot; style=&quot;height: 2em;&quot; /&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;0x1023&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;img src=&quot;/assets/2048/2x2_s2_1_3_0.svg&quot; alt=&quot;4 2 8 -&quot; style=&quot;height: 2em;&quot; /&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;0x2130&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;img src=&quot;/assets/2048/2x2_s0_3_1_2.svg&quot; alt=&quot;- 8 2 4&quot; style=&quot;height: 2em;&quot; /&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;0x0312&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;img src=&quot;/assets/2048/2x2_s1_2_0_3.svg&quot; alt=&quot;2 4 - 8&quot; style=&quot;height: 2em;&quot; /&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;0x1203&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;img src=&quot;/assets/2048/2x2_s0_1_3_2.svg&quot; alt=&quot;- 2 8 4&quot; style=&quot;height: 2em;&quot; /&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;&lt;code&gt;0x0132&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;img src=&quot;/assets/2048/2x2_s3_0_2_1.svg&quot; alt=&quot;8 - 4 2&quot; style=&quot;height: 2em;&quot; /&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;0x3021&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt; &lt;/p&gt;

&lt;p&gt;The state &lt;img src=&quot;/assets/2048/2x2_s0_1_3_2.svg&quot; alt=&quot;- 2 8 4&quot; style=&quot;height: 2em;&quot; /&gt; has the smallest number, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0x0132&lt;/code&gt;, so that is the canonical state for &lt;img src=&quot;/assets/2048/2x2_s2_3_1_0.svg&quot; alt=&quot;4 8 2 -&quot; style=&quot;height: 2em;&quot; /&gt;. In general, this system for choosing canonical states tends to put tiles with larger values toward the bottom right corner.&lt;/p&gt;

&lt;h1 id=&quot;enumeration&quot;&gt;Enumeration&lt;/h1&gt;

&lt;p&gt;Now we’re ready to start enumerating states — it’s all computation from now on. The idea is to generate all possible (canonical) start states, then for each one try each possible move, then for each move generate all possible (canonical) successor states. In code (ruby) form, the basic algorithm for enumerating states looks like this:&lt;/p&gt;

&lt;div class=&quot;language-rb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;enumerate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;board_size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;max_exponent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# Open all of the possible canonicalized start states.&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;opened&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;find_canonicalized_start_states&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;board_size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;closed&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;opened&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;any?&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# Treat opened as a stack, so this is a depth-first search.&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;opened&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;pop&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# If we&apos;ve already processed the state, or if this is&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# a win or lose state, there&apos;s nothing more to do for it.&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;next&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;closed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;member?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;next&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;win?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;max_exponent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;lose?&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# Process the state: open all of its possible canonicalized successors.&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:right&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:up&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:down&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;direction&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;move&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;direction&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;random_successors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;successor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;opened&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;successor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;canonicalize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;closed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;closed&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The code for generating start states is &lt;a href=&quot;https://github.com/jdleesmiller/twenty48/blob/479f646e81c38f1967e4fc5942617f9650d2c735/lib/twenty48/builder.rb#L59-L68&quot;&gt;here&lt;/a&gt;, and the code for the rest of the State methods, such as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;random_successors&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;canonicalize&lt;/code&gt;, is &lt;a href=&quot;https://github.com/jdleesmiller/twenty48/blob/479f646e81c38f1967e4fc5942617f9650d2c735/lib/twenty48/state.rb&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If we run this code for the game on the 2x2 board played up to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;32&lt;/code&gt; tile, which we’ve &lt;a href=&quot;/articles/2017/09/17/counting-states-combinatorics-2048.html#fnref:smallest-board&quot;&gt;previously established&lt;/a&gt; is the highest tile reachable on the 2x2 board, we get 57 states (&lt;a href=&quot;/assets/2048/enumeration_2x2_ungrouped.svg&quot;&gt;click to enlarge&lt;/a&gt;):&lt;/p&gt;
&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;/assets/2048/enumeration_2x2_ungrouped.svg&quot;&gt;&lt;img src=&quot;/assets/2048/enumeration_2x2_ungrouped.svg&quot; alt=&quot;States from the enumeration of the 2x2 game to 32&quot; /&gt;&lt;/a&gt;
&lt;/p&gt;
&lt;p&gt;In this diagram, each edge is a possible state-successor pair. For example, from the state &lt;img src=&quot;/assets/2048/2x2_s0_0_1_1.svg&quot; alt=&quot;- - 2 2&quot; style=&quot;height: 2em;&quot; /&gt;, we could swipe left or right, which would result in a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt; tile, and then the game adds a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt; tile at random, which leads to
&lt;img src=&quot;/assets/2048/2x2_s0_1_2_0.svg&quot; alt=&quot;- 2 4 -&quot; style=&quot;height: 2em;&quot; /&gt;,
&lt;img src=&quot;/assets/2048/2x2_s0_0_1_2.svg&quot; alt=&quot;- - 2 4&quot; style=&quot;height: 2em;&quot; /&gt;,
&lt;img src=&quot;/assets/2048/2x2_s0_2_2_0.svg&quot; alt=&quot;- 4 4 -&quot; style=&quot;height: 2em;&quot; /&gt; or
&lt;img src=&quot;/assets/2048/2x2_s0_0_2_2.svg&quot; alt=&quot;- - 4 4&quot; style=&quot;height: 2em;&quot; /&gt; after canonicalization;
or we could swipe up or down, which would leave the two &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt; tiles unmerged and lead to
&lt;img src=&quot;/assets/2048/2x2_s0_1_1_1.svg&quot; alt=&quot;- 2 2 2&quot; style=&quot;height: 2em;&quot; /&gt; or
&lt;img src=&quot;/assets/2048/2x2_s0_1_2_1.svg&quot; alt=&quot;- 2 4 2&quot; style=&quot;height: 2em;&quot; /&gt; after canonicalization.&lt;/p&gt;

&lt;p&gt;Not shown are the special ‘lose’ and ‘win’ states, so in total we have 59 states for the 2x2 game to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;32&lt;/code&gt; tile. This compares favorably to the estimate of 529 states from the (simple) combinatorics arguments in the previous blog post. We’ve saved about one order of magnitude!&lt;/p&gt;

&lt;p&gt;When we try to run this ruby code on the 3x3 or 4x4 boards, however, we quickly hit two problems: it’s very slow, and it runs out of memory for the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;closed&lt;/code&gt; set. I’ve included three appendices with the details of how to speed up the calculations and manage the large amounts of data involved, but first let’s see some results.&lt;/p&gt;

&lt;h1 id=&quot;results&quot;&gt;Results&lt;/h1&gt;

&lt;p&gt;The numbers of states for the various games we’ve looked at in this series of blog posts are:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Board Size&lt;/th&gt;
      &lt;th&gt;Maximum Tile&lt;/th&gt;
      &lt;th&gt;Combinatorics Bound&lt;/th&gt;
      &lt;th&gt;Actual&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;th&gt;2x2&lt;/th&gt;
      &lt;th&gt;32&lt;/th&gt;
      &lt;td align=&quot;right&quot;&gt;529&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;59&lt;/td&gt;
    &lt;/tr&gt;

    &lt;tr&gt;
      &lt;th&gt;3x3&lt;/th&gt;
      &lt;th&gt;1024&lt;/th&gt;
      &lt;td align=&quot;right&quot;&gt;786,513,819&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;25,179,014&lt;/td&gt;
    &lt;/tr&gt;

    &lt;tr&gt;
      &lt;th&gt;4x4&lt;/th&gt;
      &lt;th&gt;64&lt;/th&gt;
      &lt;td align=&quot;right&quot;&gt;2,816,814,934,817&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;40,652,843,435&lt;/td&gt;
    &lt;/tr&gt;

    &lt;tr&gt;
      &lt;th&gt;4x4&lt;/th&gt;
      &lt;th&gt;2048&lt;/th&gt;
      &lt;td align=&quot;right&quot;&gt;44,096,167,159,459,777&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;\(\gg\) 1.3 trillion&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt; &lt;/p&gt;

&lt;p&gt;Just like the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;32&lt;/code&gt; tile is the highest reachable tile on the 2x2 board, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1024&lt;/code&gt; tile &lt;a href=&quot;/articles/2017/09/17/counting-states-combinatorics-2048.html#fnref:smallest-board&quot;&gt;is the highest&lt;/a&gt; on the 3x3 board. Exhaustive enumeration of states shows that the 3x3 game contains about 25 million states, which is a factor of 31 lower than the rough ‘Combinatorics Bound’ from &lt;a href=&quot;/articles/2017/09/17/counting-states-combinatorics-2048.html&quot;&gt;the previous post&lt;/a&gt;. For the game on the 4x4 board to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;64&lt;/code&gt; tile, which is the largest game on the 4x4 board that I was able to completely enumerate, the factor is even larger, at 69. As expected, the (very basic) combinatorics bounds were quite loose, because they count many states that can’t occur in the game or are trivially related to each other.&lt;/p&gt;

&lt;p&gt;For the 3x3 game to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1024&lt;/code&gt; tile, there are too many states to draw a diagram like the one for the 2x2 game above. However, we can gain some insight into that 25 million figure by counting states in groups by (1) the sum of the tiles on the board and (2) the value of the maximum tile on the board. The sum of the tiles on the board increases by either 2 or 4 with each move &lt;sup id=&quot;fnref:property-3&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:property-3&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;, so the game generally progresses from left to right on this graph:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
&lt;a href=&quot;/assets/2048/enumeration_3x3_to_1024.svg&quot;&gt;&lt;img src=&quot;/assets/2048/enumeration_3x3_to_1024.svg&quot; alt=&quot;Number of states by tile sum and maximum tile value in the 3x3 game to 1024&quot; /&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;Early in the game, when the sum of the tiles is small, the number of states grows fairly smoothly and linearly with the sum of tiles. However, later in the game when the board fills up, there are sharp drops around where the sum of tiles reaches a larger power of two, for example at around sums 128 and 256. These drops indicate that the 3x3 game is tightly constrained by the small size of the board — there are not many ways to survive past these drops without merging most of the tiles together into a larger one.&lt;/p&gt;

&lt;p&gt;It’s also notable that the same structure seems to repeat each time a larger maximum tile is reached (that is, each time the shade of blue in the plot gets darker). The 64, 128, 256 and 512 max tile curves each have a similar slope at the start and a ‘step’ at about 26,000 states per tile sum. In terms of gameplay, this repetition reflects the fact that once you merge most of the tiles together to get the next largest one, the board is mostly empty again, except for the newly merged tile, so the game sort of ‘resets’ at that point.&lt;/p&gt;

&lt;p&gt;We might hope that the game on the 4x4 board would also show some of these characteristics, but at least up to tile sum 380, this is apparently not the case. After running the enumeration for one month and counting over 1.3 trillion states, the results to date for the full game of 2048 look like:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
&lt;a href=&quot;/assets/2048/enumeration_4x4_to_2048_partial.svg&quot;&gt;&lt;img src=&quot;/assets/2048/enumeration_4x4_to_2048_partial.svg&quot; alt=&quot;Number of states by tile sum and maximum tile value in the 4x4 game to 2048&quot; /&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;We see smooth and uninterrupted growth in the total number of states. Whereas the game on the 3x3 board topped out at about 80 thousand states per tile sum, the game on the 4x4 board shows no sign of slowing down at 27 billion states per tile sum &lt;sup id=&quot;fnref:resolve&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:resolve&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;

&lt;p&gt;We can still see the rise and fall in the number of states with each maximum tile value, and it becomes clearer if we unstack these counts and plot them on a logarithmic scale:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
&lt;a href=&quot;/assets/2048/enumeration_4x4_to_2048_partial_log.svg&quot;&gt;&lt;img src=&quot;/assets/2048/enumeration_4x4_to_2048_partial_log.svg&quot; alt=&quot;Number of states by tile sum and maximum tile value in the 4x4 game to 2048 on a log scale&quot; /&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;The top line in black shows the total number of states, summing over all the maximum tile values, which are again shown in shades of blue. Each blue arc shows the growth and later decay in the number of states with a given maximum tile value, as the game progresses. The bending down of the total (black line) shows that the growth in the number of states per tile sum tapers off as the game progresses, but the numbers are already quite large.&lt;/p&gt;

&lt;h1 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h1&gt;

&lt;p&gt;We’ve improved our estimates for the number of states in the game of 2048 on the 2x2 and 3x3 boards by one to two orders of magnitude, compared to the previous (basic) combinatorial estimates. The number of states for the game on the 4x4 board remains too large to enumerate in full, but we have at least managed to completely enumerate the states for the 4x4 game to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;64&lt;/code&gt; tile, and we’ve made an attempt at enumerating the states for the full game.&lt;/p&gt;

&lt;p&gt;The explicit enumeration of states counts only states that can be reached in actual game play, but even with that restriction there are still some surprising states included in the count. For example, in the last figure above, the arc that shows the number of states with at most a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;32&lt;/code&gt; tile does not stop until tile sum 348. There are four states with tile sum 348 and no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;64&lt;/code&gt; tile, one of which I chose for the cover image of this post: &lt;sup id=&quot;fnref:cover&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:cover&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
&lt;img src=&quot;/assets/2048/4x4_s2_3_4_5_4_5_4_5_5_4_5_4_4_5_4_5.svg&quot; alt=&quot;State from layer 348 with no 64 tile; contains diagonal bands of 16s and 32s&quot; /&gt;
&lt;/p&gt;

&lt;p&gt;It contains seven &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;32&lt;/code&gt; tiles and seven &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;16&lt;/code&gt; tiles arranged in a nice striped pattern that makes it very difficult to merge any of them. I find it quite surprising that it’s possible to play to such a state without losing, and indeed it seems quite improbable, especially if one is ‘playing well’.&lt;/p&gt;

&lt;p&gt;In the next post, we’ll explore what it means to ‘play well’ in a rigorous way by modeling the game of 2048 as a &lt;a href=&quot;https://en.wikipedia.org/wiki/Markov_decision_process&quot;&gt;Markov Decision Process&lt;/a&gt; and finding an &lt;em&gt;optimal policy&lt;/em&gt; for the 2x2 and 3x3 games — that is, we will find a strategy for playing those game that we can show mathematically to be at least as good as any other possible strategy.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt; &lt;/p&gt;

&lt;h1 id=&quot;appendix-a-bit-bashing-for-efficiency&quot;&gt;Appendix A: Bit Bashing for Efficiency&lt;/h1&gt;

&lt;p&gt;Profiling the ruby code above revealed that most of the time was being spent on state manipulation — for example, moving tiles, counting available tiles, and reflecting or rotating states to find canonical states. Let’s see how we can speed it up.&lt;/p&gt;

&lt;p&gt;The hexadecimal numerical representation for states has a convenient property: for the 4x4 board, we need to store 16 numbers, where each number takes 4 bits, for a total of 64 bits. Now that we all use 64-bit computers, this is an auspicious number: the whole board state can fit into a single 64-bit (8-byte) machine word. For example, the 4x4 state (which is a winning state, because it has a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2048&lt;/code&gt; tile)&lt;/p&gt;
&lt;p align=&quot;center&quot;&gt;&lt;img src=&quot;/assets/2048/4x4_s0_2_2_11_0_1_3_0_2_0_1_0_0_0_0_0.svg&quot; alt=&quot;- 4 4 2048 - 2 8 - 4 - 2 - - - - -&quot; /&gt;&lt;/p&gt;
&lt;p&gt;would be written &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0x022b013020100000&lt;/code&gt; in hexadecimal, or more clearly with the addition of line breaks to make a 4x4 grid:&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;022b
0130
2010
0000
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;With the state represented as a 64-bit integer, we can also implement many common manipulations on states very efficiently using bit mask and shift operations, often without loops &lt;sup id=&quot;fnref:bit-bashing&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:bit-bashing&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;4&lt;/a&gt;&lt;/sup&gt;. For example, the C++ function to reflect a 4x4 board state horizontally looks like:&lt;/p&gt;
&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kt&quot;&gt;uint64_t&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;reflect_horizontally&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;uint64_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kt&quot;&gt;uint64_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;c1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0xF000F000F000F000ULL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;c2&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0x0F000F000F000F00ULL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;c3&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0x00F000F000F000F0ULL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;c4&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0x000F000F000F000FULL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;12&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c2&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c3&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c4&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;12&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;While initially quite opaque, all this is doing is shuffling bits around. The role of first bit mask, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0xF000F000F000F000ULL&lt;/code&gt;, becomes clearer if we omit the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0x&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ULL&lt;/code&gt;, which just tell the compiler that this is non-negative 64-bit integer in hexadecimal, and again add line breaks to make a 4x4 grid:&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;F000
F000
F000
F000
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;The binary representation of the hexadecimal digit &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;F&lt;/code&gt; is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1111&lt;/code&gt;, so the effect of this bit mask is to make &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;c1&lt;/code&gt; contain only the values in the first column of the board and zero bits everywhere else. The bit shift &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;c1 &amp;gt;&amp;gt; 12&lt;/code&gt; at the end of the function moves the first column 12 bits, which is to say three 4-bit cells, to the right, which makes it the last column. Similarly, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;c2&lt;/code&gt; selects the second column, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;c2 &amp;gt;&amp;gt; 4&lt;/code&gt; moves it one cell to the right, and so on with the third and fourth columns to reverse the order of the columns.&lt;/p&gt;

&lt;p&gt;Some functions take a bit more work to decipher. If you’re in the mood for a puzzle, here’s a function that counts the number of cells available (value zero) in a 4x4 state (you can find &lt;a href=&quot;https://github.com/jdleesmiller/twenty48/blob/479f646e81c38f1967e4fc5942617f9650d2c735/ext/twenty48/state.hpp#L68-L83&quot;&gt;my explanation in comments here&lt;/a&gt;):&lt;/p&gt;
&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;cells_available&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;uint64_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;=&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0x1111111111111111ULL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;kt&quot;&gt;uint64_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0x1111111111111111ULL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;60&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;count&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;16&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Profiling (with &lt;a href=&quot;https://en.wikipedia.org/wiki/Perf_(Linux)&quot;&gt;perf&lt;/a&gt;) showed that such tricks made a big difference — compared to the obvious implementations with arrays and loops, these functions require very few CPU instructions and contain few or no branches, which allows the CPU to keep its &lt;a href=&quot;https://en.wikipedia.org/wiki/Instruction_pipelining&quot;&gt;instruction pipelines&lt;/a&gt; full.&lt;/p&gt;

&lt;h1 id=&quot;appendix-b-layers-and-mapreduce-for-parallelism&quot;&gt;Appendix B: Layers and MapReduce for Parallelism&lt;/h1&gt;

&lt;p&gt;The next challenge is that we can’t keep the whole &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;closed&lt;/code&gt; set in memory. To break up the state space into manageably sized pieces, we can use the following property of the game: &lt;em&gt;The sum of the tiles on the board increases by either 2 or 4 with each move.&lt;/em&gt; This property holds because merging two tiles does not change the sum of the tiles on the board, and the game then adds either a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt; or a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt; tile. &lt;sup id=&quot;fnref:property-3:1&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:property-3&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;This property is useful here because it means that we can organize the states into &lt;em&gt;layers&lt;/em&gt; according to the sum of their tiles. We can therefore generate the whole state space by working through a single layer at a time, rather than having to deal with the whole state space at once.&lt;/p&gt;

&lt;p&gt;To parallelize the work within each layer, we can use the &lt;a href=&quot;https://en.wikipedia.org/wiki/MapReduce&quot;&gt;MapReduce&lt;/a&gt; concept made famous by Google. The ‘map’ step here is to take one complete layer of states with sum \(s\) and break it up into pieces; then, for each piece in parallel, generate all of the successor states, which will have either sum \(s + 2\) or \(s + 4\). The ‘reduce’ step is to merge all of the pieces for the layer with sum \(s + 2\) together into a complete layer, removing any duplicates. The pieces with sum \(s + 4\) are retained until the next layer, in which states have sum \(s + 2\), is processed, at which point they will be included in the merge. This may be easier to see in an animation (made with &lt;a href=&quot;https://d3js.org&quot;&gt;d3&lt;/a&gt;):&lt;/p&gt;

&lt;div id=&quot;map-reduce&quot;&gt;&lt;/div&gt;

&lt;p&gt;In the map step for each piece, it is feasible to maintain the set of successor states in memory. To make the merge in the reduce step efficient, we want the map step to output a list of states for each piece in order by their state numbers so we can merge the pieces together in linear time. Here Google again comes to our aid: they have released a handy in-memory &lt;a href=&quot;https://code.google.com/archive/p/cpp-btree/&quot;&gt;B-tree implementation&lt;/a&gt; that plays well with the C++ standard template library. &lt;a href=&quot;https://en.wikipedia.org/wiki/B-tree&quot;&gt;B-trees&lt;/a&gt; are most commonly found in relational database systems, where they are often used to maintain indexes on columns. They keep data in order and provide logarithmic time lookup and also logarithmic time insertion — much better than logarithmic plus linear time insertion into a sorted list — with relatively little memory overhead.&lt;/p&gt;

&lt;p&gt;To break the state space up into even smaller pieces, which reduces the amount of work we need to do in each merge step, we can exploit an another property of the game: &lt;em&gt;the maximum tile value on the board must either stay the same or double with each move.&lt;/em&gt; This property holds because the maximum tile value never decreases, and when it does increase, it can only increase as the result of merging two tiles — that is, even if you have for example four &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;16&lt;/code&gt; tiles in a row and you merge them, after one move the result is two &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;32&lt;/code&gt; tiles, not one &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;64&lt;/code&gt; tile.&lt;/p&gt;

&lt;p&gt;Together with the property above, this means that from a list of states in which all states have tile sum \(s\) and maximum tile value \(k\), the generated successors will all fall into one of four pieces:&lt;/p&gt;

&lt;table&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;th align=&quot;right&quot;&gt;Piece&lt;/th&gt;
      &lt;th align=&quot;right&quot;&gt;1&lt;/th&gt;
      &lt;th align=&quot;right&quot;&gt;2&lt;/th&gt;
      &lt;th align=&quot;right&quot;&gt;3&lt;/th&gt;
      &lt;th align=&quot;right&quot;&gt;4&lt;/th&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;th align=&quot;right&quot;&gt;Tile Sum&lt;/th&gt;
      &lt;td align=&quot;right&quot;&gt;\(s+2\)&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;\(s+2\)&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;\(s+4\)&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;\(s+4\)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;th align=&quot;right&quot;&gt;Max Tile Value&lt;/th&gt;
      &lt;td align=&quot;right&quot;&gt;\(k\)&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;\(2k\)&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;\(k\)&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;\(2k\)&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;There is a bit more bookkeeping to keep track of which pieces need to be merged together at each step, but it is basically the same idea. Here are the 57 states (excluding the ‘win’ and ‘lose’ states) for the 2x2 game to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;32&lt;/code&gt; tile again, this time with the states grouped by tile sum and maximum tile value, here written \(s / k\):&lt;/p&gt;
&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;/assets/2048/enumeration_2x2_grouped.svg&quot;&gt;&lt;img src=&quot;/assets/2048/enumeration_2x2_grouped.svg&quot; alt=&quot;States from the enumeration of the 2x2 game to 32 with grouping into parts by sum and max value&quot; /&gt;&lt;/a&gt;
&lt;/p&gt;
&lt;p&gt;For example, in the leftmost piece, both states have tile sum 4 and maximum tile value &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt;. The transitions from that piece are to pieces with tile sum either 6 or 8 and maximum tile value &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt;. Compared to the original diagram of these states without grouping, it’s also easier to see that you always transition to a state with a tile sum that is either 2 or 4 larger, like in the &lt;a href=&quot;/articles/2017/08/05/markov-chain-2048.html&quot;&gt;first post&lt;/a&gt; in this series.&lt;/p&gt;

&lt;h1 id=&quot;appendix-c-encoding-and-compression&quot;&gt;Appendix C: Encoding and Compression&lt;/h1&gt;

&lt;p&gt;When working with billions or trillions of states, and each state takes 8 bytes, even fitting them all on disk is not trivial (or at least not cheap). To reduce the storage space required, and also the amount of input/output required, we can exploit the fact that the states are stored as sorted lists of integers — rather than storing each integer in full, we can store the differences between successive integers. These differences will generally be smaller than the integers themselves, so it is usually possible to store the differences in a smaller number of bytes. Using a &lt;a href=&quot;https://en.wikipedia.org/wiki/Variable-width_encoding&quot;&gt;variable-width encoding scheme&lt;/a&gt; to store the differences ensures that we use only the number of bytes we need. &lt;sup id=&quot;fnref:variable-width&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:variable-width&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;5&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;For example, the list of states for the 4x4 game to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2048&lt;/code&gt; tile with tile sum 380 and maximum tile value &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;128&lt;/code&gt; contains 21,705,361,721 states &lt;sup id=&quot;fnref:resolve:1&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:resolve&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;. At 8 bytes per state, that would be roughly 161GiB. However, with variable-width encoding, it takes only 35GiB — a compression factor of 4.6 — or about 1.7 bytes per state.&lt;/p&gt;

&lt;p&gt;For longer term storage and transport over Internet, I also &lt;a href=&quot;/articles/2017/05/01/compression-pareto-docker-gnuplot.html&quot;&gt;tried several compression programs&lt;/a&gt; on the variable-width encoded data. To evaluate the different programs, I used a smaller list of states with tile sum 260 and maximum tile value &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;32&lt;/code&gt;; it contained 30,954,422 states, which is about 1/700th the number in the 380/128 layer mentioned above, and it weighed in at 64MiB after variable-width encoding. For each of the programs, and for each of their supported compression levels, I measured the elapsed time for compression and the resulting compressed size using this smaller list of states.&lt;/p&gt;

&lt;p&gt;Three programs emerged on the resulting &lt;a href=&quot;/articles/2017/05/01/compression-pareto-docker-gnuplot.html&quot;&gt;Pareto frontier&lt;/a&gt;: Facebook’s &lt;a href=&quot;https://github.com/facebook/zstd&quot;&gt;Zstandard&lt;/a&gt;, Google’s &lt;a href=&quot;https://github.com/google/brotli&quot;&gt;Brotli&lt;/a&gt; and &lt;a href=&quot;http://www.7-zip.org/&quot;&gt;7-zip&lt;/a&gt;. After scaling size and time up by a factor of 700 to estimate performance on the larger 380/128 layer, the frontier looks like:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
&lt;a href=&quot;/assets/2048/sum-0260.max_value-5.scale-701.svg&quot;&gt;&lt;img src=&quot;/assets/2048/sum-0260.max_value-5.scale-701.svg&quot; alt=&quot;Pareto frontier for selecting a compression program for lists of 2048 states&quot; /&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;Here closer to the origin is better; we see Zstandard performing best for relatively fast and light compression, and 7zip performing best for relatively slow and heavy compression. From the graph, we can see that Zstandard at compression level 11 is fairly close to the origin, and it turned out to minimize the particular linear cost function that I used. The results on the smaller layer suggested that Zstandard level 11 would reduce the 380/128 layer from 35GiB to about 10GiB. In fact, it did much better: it reduced it from 35GiB to 3.8GiB, for another factor of 9.2, or about 1.5 &lt;em&gt;bits&lt;/em&gt; per state. I think that’s quite remarkable.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt; &lt;/p&gt;

&lt;p&gt;Thanks to &lt;a href=&quot;https://twitter.com/h0peth0mas&quot;&gt;Hope Thomas&lt;/a&gt; for reviewing drafts of this article.&lt;/p&gt;

&lt;p&gt;If you’ve read this far, perhaps you should &lt;a href=&quot;https://twitter.com/jdleesmiller&quot;&gt;follow me on twitter&lt;/a&gt;, or even apply to work at &lt;a href=&quot;https://www.overleaf.com/jobs&quot;&gt;Overleaf&lt;/a&gt;. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:)&lt;/code&gt;&lt;/p&gt;

&lt;h1 id=&quot;footnotes&quot;&gt;Footnotes&lt;/h1&gt;

&lt;script type=&quot;text/x-mathjax-config&quot;&gt;
MathJax.Hub.Config({
  TeX: { equationNumbers: { autoNumber: &quot;AMS&quot; } }
});
&lt;/script&gt;

&lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML&quot; type=&quot;text/javascript&quot;&gt;&lt;/script&gt;

&lt;script src=&quot;https://d3js.org/d3.v4.min.js&quot;&gt;&lt;/script&gt;

&lt;script src=&quot;/assets/2048/map_reduce.js&quot;&gt;&lt;/script&gt;

&lt;!--
To make the state diagram:

Go to http://gabrielecirulli.github.io/2048/
Open dev console
gm = new GameManager(4, KeyboardInputManager, HTMLActuator, LocalStorageManager)
// Remove the two random tiles (if needed); for me they were:
gm.grid.removeTile(new Tile({x: 0, y: 2}))
gm.grid.removeTile(new Tile({x: 3, y: 3}))
gm.actuate()
// Board should be empty
gm.grid.insertTile(new Tile({x: 0, y: 0}, 4))
gm.grid.insertTile(new Tile({x: 1, y: 0}, 8))
gm.grid.insertTile(new Tile({x: 2, y: 0}, 16))
gm.grid.insertTile(new Tile({x: 3, y: 0}, 32))

gm.grid.insertTile(new Tile({x: 0, y: 1}, 16))
gm.grid.insertTile(new Tile({x: 1, y: 1}, 32))
gm.grid.insertTile(new Tile({x: 2, y: 1}, 16))
gm.grid.insertTile(new Tile({x: 3, y: 1}, 32))

gm.grid.insertTile(new Tile({x: 0, y: 2}, 32))
gm.grid.insertTile(new Tile({x: 1, y: 2}, 16))
gm.grid.insertTile(new Tile({x: 2, y: 2}, 32))
gm.grid.insertTile(new Tile({x: 3, y: 2}, 16))

gm.grid.insertTile(new Tile({x: 0, y: 3}, 16))
gm.grid.insertTile(new Tile({x: 1, y: 3}, 32))
gm.grid.insertTile(new Tile({x: 2, y: 3}, 16))
gm.grid.insertTile(new Tile({x: 3, y: 3}, 32))

gm.actuate()
// Board should have the state.
--&gt;
&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:property-3&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;This property was called called &lt;em&gt;Property 3&lt;/em&gt; in the &lt;a href=&quot;/articles/2017/09/17/counting-states-combinatorics-2048.html&quot;&gt;previous post&lt;/a&gt;, and we also used it anonymously in the &lt;a href=&quot;/articles/2017/08/05/markov-chain-2048.html&quot;&gt;first post&lt;/a&gt;. It is a very useful property. &lt;a href=&quot;#fnref:property-3&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt; &lt;a href=&quot;#fnref:property-3:1&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:resolve&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;These figures come from the one month build on the full 4x4 game, which used an additional technique for reducing the size of the state space: for each state, it looked one move ahead to see whether the next move was a definite win or a definite loss; if it was, the state was collapsed to a special ‘one move to win’ or ‘one move to lose’ state. This technique was not very effective, however, because states that are close to a loss tend to have few very successors anyway, and because win states only occur much later in the game, and the enumeration process never reached a win state. I therefore haven’t included it in the main body of the article, but the &lt;a href=&quot;https://github.com/jdleesmiller/twenty48/blob/479f646e81c38f1967e4fc5942617f9650d2c735/ext/twenty48/valuer.hpp&quot;&gt;code is here&lt;/a&gt;. The state counts mentioned in this article for the partial enumeration of states with the 4x4 board to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2048&lt;/code&gt; tile are slightly reduced compared to what they would be without this technique, but only slightly. &lt;a href=&quot;#fnref:resolve&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt; &lt;a href=&quot;#fnref:resolve:1&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:cover&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;In case you are wondering, I faked that screenshot; I didn’t actually play the game to that state! &lt;a href=&quot;#fnref:cover&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:bit-bashing&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;The bit bashing techniques used here are based mainly on &lt;a href=&quot;https://github.com/nneonneo/2048-ai/blob/1f25f2e19e82a477600fd1b437e710d584f99e99/2048.cpp&quot;&gt;2048-ai&lt;/a&gt; by Robert Xiao and &lt;a href=&quot;https://github.com/kcwu/2048-c/blob/209a1f5ea635222a859c493a90d3304328117af3/micro_optimize.cc&quot;&gt;2048-c&lt;/a&gt; by Kuang-che Wu, with some help from &lt;a href=&quot;http://graphics.stanford.edu/~seander/bithacks.html&quot;&gt;Bit Twiddling Hacks&lt;/a&gt; by Sean Eron Anderson. The original motivation for 2048-ai and 2048-c was to allow real time search for 2048 AI bots. &lt;a href=&quot;#fnref:bit-bashing&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:variable-width&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;Variable width encodings are most commonly encountered in the &lt;a href=&quot;https://en.wikipedia.org/wiki/UTF-8&quot;&gt;UTF-8&lt;/a&gt; character encoding for unicode, which you are using as you read this webpage. They are also sometimes used for storing &lt;a href=&quot;https://en.wikipedia.org/wiki/Database_index&quot;&gt;integer primary key indexes&lt;/a&gt; in some databases, which also face the problem of efficiently storing large lists of sorted integers. The implementation used here is &lt;a href=&quot;https://github.com/cruppstahl/libvbyte&quot;&gt;libvbyte&lt;/a&gt; from Christoph Rupp, based on work by &lt;a href=&quot;https://github.com/lemire/MaskedVbyte&quot;&gt;Daniel Lemire&lt;/a&gt;. &lt;a href=&quot;#fnref:variable-width&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</description>
        <pubDate>Sun, 10 Dec 2017 00:00:00 +0000</pubDate>
        <link>http://jdlm.info/articles/2017/12/10/counting-states-enumeration-2048.html</link>
        <guid isPermaLink="true">http://jdlm.info/articles/2017/12/10/counting-states-enumeration-2048.html</guid>
        
        
        <category>articles</category>
        
      </item>
    
      <item>
        <title>The Mathematics of 2048: Counting States with Combinatorics</title>
        <description>&lt;p&gt;&lt;strong&gt;Updates&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2017-09-25&lt;/strong&gt; There was some lively &lt;a href=&quot;https://news.ycombinator.com/item?id=15327837&quot;&gt;discussion about this article on Hacker News&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post is the second in a series. Next: &lt;a href=&quot;/articles/2017/12/10/counting-states-enumeration-2048.html&quot;&gt;Counting States by Exhaustive Enumeration&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt; &lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/2048/2048_infeasible.png&quot; alt=&quot;Screenshot of 2048 with an infeasible board configuration&quot; style=&quot;width: 40%; float: right; margin-left: 10pt; border: 6pt solid #eee;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;In &lt;a href=&quot;/articles/2017/08/05/markov-chain-2048.html&quot;&gt;my last 2048 post&lt;/a&gt;, I found that it takes at least 938.8 moves on average to win a game of &lt;a href=&quot;http://gabrielecirulli.github.io/2048&quot;&gt;2048&lt;/a&gt;. The main simplification that enabled that calculation was to ignore the structure of the board — essentially to throw the tiles into a bag instead of placing them on a board. With the ‘bag’ simplification, we were able to model the game as a Markov chain with only 3486 distinct states.&lt;/p&gt;

&lt;p&gt;In this post, we’ll make a first cut at counting the number of states without the bag simplification. That is, in this post a &lt;em&gt;state&lt;/em&gt; captures the complete configuration of the board by specifying which tile, if any, is in each of the board’s cells. We would therefore expect there to be a lot more states of this kind, now that the positions of the tiles (and cells without tiles) are included, and we will see that this is indeed the case.&lt;/p&gt;

&lt;p&gt;To do so, we will use some (simple) techniques from enumerative combinatorics to exclude some states that we can write down but which can’t actually occur in the game, such as the one above. The results will also apply to 2048-like games played on different boards (not just 4x4) and up to different tiles (not just the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2048&lt;/code&gt; tile). We’ll see that such games on smaller boards and/or to smaller tiles have far fewer states than the full 4x4 game to 2048, and that the techniques used here are relatively much more effective at reducing the estimated number of states when the board size is small. As a bonus, we’ll also see that the 4x4 board is the smallest square board on which it is possible to reach the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2048&lt;/code&gt; tile.&lt;/p&gt;

&lt;p&gt;The (research quality) code behind this article is &lt;a href=&quot;https://github.com/jdleesmiller/twenty48&quot;&gt;open source&lt;/a&gt;, in case you would like to see the &lt;a href=&quot;https://github.com/jdleesmiller/twenty48/blob/4337c357f2cc14bdc3e14ddaa5207ad2a6a972e6/bin/combinatorics&quot;&gt;implementation&lt;/a&gt; or &lt;a href=&quot;https://github.com/jdleesmiller/twenty48/tree/4337c357f2cc14bdc3e14ddaa5207ad2a6a972e6/data/combinatorics&quot;&gt;code for the plots&lt;/a&gt;.&lt;/p&gt;

&lt;h1 id=&quot;baseline&quot;&gt;Baseline&lt;/h1&gt;

&lt;p&gt;The most straightforward way to estimate the number of states in 2048 is to observe that there are 16 cells, and each cell can either be blank or contain a tile with a value that is one of the 11 powers of 2 from 2 to 2048. That gives 12 possibilities for each of the 16 cells, for a total of \(12^{16}\), or 184 quadrillion (~\(10^{17}\)), possible states that we can write in this way. For comparison, &lt;a href=&quot;https://tromp.github.io/chess/chess.html&quot;&gt;some estimates&lt;/a&gt; put the number of possible board configurations for the game of chess at around \(10^{45}\) states, and &lt;a href=&quot;https://en.wikipedia.org/wiki/Go_and_mathematics#Complexity_of_certain_Go_configurations&quot;&gt;the latest estimates&lt;/a&gt; for the game of Go are around \(10^{170}\) states, so while \(10^{17}\) is large, it’s certainly not the largest as games go.&lt;/p&gt;

&lt;p&gt;For 2048-like games more generally, let \(B\) be the board size, and let \(K\) be the exponent of the winning tile with value \(2^K\). For convenience, let \(C\) denote the number of cells on the board, so \(C=B^2\). For the usual 4x4 game to 2048, \(B=4\), \(C=16\), and \(K = 11\), since \(2^{11} = 2048\), and our estimate for the number of states is \[(K + 1)^C.\] Now let’s see how we can refine this estimate.&lt;/p&gt;

&lt;p&gt;First, since the game ends when we obtain a \(2^K\) tile, we don’t particularly care about where that tile is or what else is on the board. We can therefore condense all of the states with a \(2^K\) tile into a special “win” state. In the remaining states, each cell can either be blank or hold one of \(K - 1\) tiles. This reduces the number of states we have to worry about to \[K^C + 1\] where the \(1\) is for the win state.&lt;/p&gt;

&lt;p&gt;Second, we can observe that some of those \(K^C\) states can never occur in the game. In particular, the rules of the game imply two useful properties:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Property 1:&lt;/strong&gt; There are always at least two tiles on the board.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Property 2:&lt;/strong&gt; There is always at least one &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt; tile on the board.&lt;/p&gt;

&lt;p&gt;The first property holds because even if you start with two tiles and merge them, there is still one left, and then the game adds a random tile, leaving two tiles. The second property holds because the game always adds a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt; tile after each move.&lt;/p&gt;

&lt;p&gt;We therefore know that in any valid state there must be at least two tiles on the board, and that one of them must be a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt; tile. To account for this, we can subtract all states with no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt; tile, of which are \((K-2)^C\), and also the states with just one &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt; tile and all other cells empty, of which there are \(C\), and the states with only one &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt; tile and all other cells empty, of which there are again \(C\). This gives an estimate of
\[K^C - (K-2)^C - 2C + 1\]
states in total. Of course, when \(K\) or \(C\) is large, this looks pretty much just like \(K^C\), which is the dominant term, but this correction is more significant for smaller values.&lt;/p&gt;

&lt;p&gt;Let’s use this formula to tabulate the estimated number of states various board sizes and maximum tiles:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Maximum Tile&lt;/th&gt;
      &lt;th colspan=&quot;3&quot;&gt;Board Size&lt;/th&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;th&gt;&lt;/th&gt;
      &lt;th align=&quot;right&quot;&gt;2x2&lt;/th&gt;
      &lt;th align=&quot;right&quot;&gt;3x3&lt;/th&gt;
      &lt;th align=&quot;right&quot;&gt;4x4&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td align=&quot;right&quot;&gt;8&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;73&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;19,665&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;43,046,689&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td align=&quot;right&quot;&gt;16&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;233&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;261,615&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;4,294,901,729&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td align=&quot;right&quot;&gt;32&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;537&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;1,933,425&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;152,544,843,873&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td align=&quot;right&quot;&gt;64&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;1,033&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;9,815,535&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;2,816,814,940,129&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td align=&quot;right&quot;&gt;128&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;1,769&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;38,400,465&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;33,080,342,678,945&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td align=&quot;right&quot;&gt;256&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;2,793&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;124,140,015&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;278,653,866,803,169&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td align=&quot;right&quot;&gt;512&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;4,153&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;347,066,865&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;1,819,787,258,282,209&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td align=&quot;right&quot;&gt;1024&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;5,897&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;865,782,255&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;9,718,525,023,289,313&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td align=&quot;right&quot;&gt;2048&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;8,073&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;1,970,527,185&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;44,096,709,674,720,289&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;We can see immediately that the 2x2 and 3x3 games have many orders of magnitude fewer states than the 4x4 game. We’ve also also managed to reduce our estimate for the number of tiles in the 4x4 game to 2048 to “only” 44 quadrillion, or ~\(10^{16}\).&lt;/p&gt;

&lt;h1 id=&quot;counting-in-layers&quot;&gt;Counting in Layers&lt;/h1&gt;

&lt;p&gt;To gain some additional insight into these state counts, we can take advantage of another important property, which was also useful in the last post:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Property 3:&lt;/strong&gt; The sum of the tiles on the board increases by either 2 or 4 with each move.&lt;/p&gt;

&lt;p&gt;This holds because merging two tiles does not change the sum of the tiles on the board, and the game then adds either a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt; or a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt; tile.&lt;/p&gt;

&lt;p&gt;Property 3 implies that states never repeat in the course of a game. This means that we can organize the states into &lt;em&gt;layers&lt;/em&gt; according to the sum of their tiles. If the game is in a state in the layer with sum 10, we know that the next state must be in the layer with either sum 12 or sum 14. It turns out we can also count the number of states in each layer, as follows.&lt;/p&gt;

&lt;p&gt;Let \(S\) denote the sum of the tiles on the board. We want to count the number of ways that up to \(C\) numbers, each of which is a power of 2 between 2 and \(2^{K-1}\), can be added together to produce \(S\).&lt;/p&gt;

&lt;p&gt;Fortunately, this turns out to be a variation on a well-studied problem in combinatorics: counting the &lt;a href=&quot;https://en.wikipedia.org/wiki/Composition_(combinatorics)&quot;&gt;compositions of an integer&lt;/a&gt;. In general, a composition of an integer \(S\) is an ordered collection of integers that sum to \(S\); each integer in the collection is called a &lt;em&gt;part&lt;/em&gt;. For example, there are four compositions of the integer \(3\), namely \(1 + 1 + 1\), \(1 + 2\), \(2 + 1\) and \(3\). When there are restrictions on the parts, such as being a power of two and only having a certain number of parts, the term is a &lt;em&gt;restricted&lt;/em&gt; composition.&lt;/p&gt;

&lt;p&gt;Even more fortunately, Chinn and Niederhausen (2004) &lt;sup id=&quot;fnref:Chinn&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:Chinn&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; have already studied exactly this kind of restricted composition and derived a recurrence that allows us count the number of compositions in which there are a specific number of parts, and each part is a power of 2. Let \(N(s, c)\) denote the number of compositions of a (positive) integer \(s\) into exactly \(c\) parts where each part is a power of 2. It then holds that
\[
N(s, c) = \begin{cases}
\sum_{i = 0}^{\lfloor \log_2 s \rfloor} N(s - 2^i, c - 1), &amp;amp; 2 \le c \le s \\\&lt;br /&gt;
1, &amp;amp; c = 1 \textrm{ and } s \textrm{ is a power of 2} \\\&lt;br /&gt;
0, &amp;amp; \textrm{otherwise}
\end{cases}
\]
because for every composition of \(s - 2^i\) into \(c - 1\) parts, we can obtain a composition of \(s\) with \(c\) parts by adding one part with value \(2^i\).&lt;/p&gt;

&lt;p&gt;We now just need to make a few minor adjustments to the summation bounds: we would like to use powers of 2 starting at 2 and at most \(2^{K-1}\), since if we have a \(2^K\) tile the game is won. To this end, let \(N_m(s, c)\) denote the number of compositions of \(s\) into exactly \(c\) parts where each part is a power of 2 between \(2^m\) and \(2^{K-1}\). This is given by
\[
N_m(s, c) = \begin{cases}
\sum_{i = m}^{K - 1} N(s - 2^i, c - 1), &amp;amp; 2 \le c \le s \\\&lt;br /&gt;
1, &amp;amp; c = 1 \textrm{ and }
     s = 2^i \textrm{ for some } i \in \{ m, \ldots, K-1 \} \\\&lt;br /&gt;
0, &amp;amp; \textrm{otherwise}
\end{cases}
\]
following the same logic as above.&lt;/p&gt;

&lt;p&gt;Now we have a formula for exactly \(c\) parts, but we want a formula for up to \(c\) parts. We can follow the same rationale as in the previous section: subtract off the states with no 2 or 4 tile, of which there are \(N_3(s, c)\). According to property 1, we need at least 2 parts, so we start summing at \(c=2\). This gives
\[
\sum_{c = 2}^{C} {C \choose c} \left( N_1(s, c) - N_3(s, c) \right)
\]
as our estimate for the number of states with sum \(s\). Here \(C \choose c\) is a &lt;a href=&quot;https://en.wikipedia.org/wiki/Binomial_coefficient&quot;&gt;binomial coefficient&lt;/a&gt; that gives the number ways of choosing \(c\) of the possible \(C\) cells into which to place the tiles. Let’s plot it out.&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
&lt;img src=&quot;/assets/2048/combinatorics_layers_summary.png&quot; alt=&quot;Number of states by sum of tiles (with K=11)&quot; /&gt;
&lt;/p&gt;

&lt;p&gt;In terms of magnitude, we can see that the 2x2 game never has more than 60 states in any layer, the 3x3 game peaks at about 3 million states per layer, and the 4x4 game peaks at about 32 trillion (\(10^{13}\)) states per layer. The number of states grows rapidly early in the game but then tapers off and eventually decreases as the board fills up. On the decreasing portion of the curve, we see discontinuities: particularly for higher sums, it may happen that there are no tiles that will fit on the board and sum to that value.&lt;/p&gt;

&lt;p&gt;The upper limit on the horizontal axis arises because we can have \(C\) values, each up to \(2^{K-1}\), so the maximum achievable sum is \(C 2^{K-1}\), or 16,384 for the 4x4 game to 2048.&lt;/p&gt;

&lt;p&gt;Finally, it’s worth noting that if we sum the number of states in each layer over all of the possible layer sums from 4 to \(C 2^{K-1}\), and add one for the special win state, we get the same number of states as we estimated in the previous section, which is a helpful sanity check.&lt;/p&gt;

&lt;h3 id=&quot;layer-reachability&quot;&gt;Layer Reachability&lt;/h3&gt;

&lt;p&gt;Another useful consequence of Property 3 is that if two consecutive layers have no states, it’s not possible to reach later layers. This is because the sum can increase by at most 4 per turn; if there are two adjacent layers with no states, then the sum would have to increase by 6 in a single move in order to ‘jump’ to the subsequent layer, which is not possible. Finding the layer sums that contain no states according to the calculation above therefore allows us to tighten up our estimate by excluding states in unreachable layers after the last reachable layer. The largest reachable layer sums (without ever attaining a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2048&lt;/code&gt; tile) are:&lt;/p&gt;

&lt;table style=&quot;width: auto;&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Board Size&lt;/th&gt;
      &lt;th&gt;Largest Reachable Layer Sum&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;2x2&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;60&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;3x3&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;2,044&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;4x4&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;9,212&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;This table also tells us that the highest tile we can reach on the 2x2 board is the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;32&lt;/code&gt; tile, because the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;64&lt;/code&gt; tile can’t occur in a layer with sum 60 or less, and similarly highest reachable tile on the 3x3 board is the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1024&lt;/code&gt; tile. This means that the 4x4 board is the smallest square board on which it’s possible to reach the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2048&lt;/code&gt; tile &lt;sup id=&quot;fnref:smallest-board&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:smallest-board&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;. For the 4x4 board, the largest layer sum we can reach without reaching a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2048&lt;/code&gt; tile (and therefore winning) is 9,212, but larger sums would be reachable if we did allow a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2048&lt;/code&gt; tile.&lt;/p&gt;

&lt;p&gt;Taking into account layer reachability, the new estimates for the number of states are:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Maximum Tile&lt;/th&gt;
      &lt;th&gt;Method&lt;/th&gt;
      &lt;th colspan=&quot;3&quot;&gt;Board Size&lt;/th&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;th&gt;&lt;/th&gt;
      &lt;th&gt;&lt;/th&gt;
      &lt;th align=&quot;right&quot;&gt;2x2&lt;/th&gt;
      &lt;th align=&quot;right&quot;&gt;3x3&lt;/th&gt;
      &lt;th align=&quot;right&quot;&gt;4x4&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;

    &lt;tr&gt;
      &lt;th align=&quot;right&quot; valign=&quot;top&quot; rowspan=&quot;2&quot;&gt;8&lt;/th&gt;
      &lt;td&gt;Baseline&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;73&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;19,665&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;43,046,689&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Layer Reachability&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;73&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;19,665&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;43,046,689&lt;/td&gt;
    &lt;/tr&gt;

    &lt;tr&gt;
      &lt;th align=&quot;right&quot; valign=&quot;top&quot; rowspan=&quot;2&quot;&gt;16&lt;/th&gt;
      &lt;td&gt;Baseline&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;233&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;261,615&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;4,294,901,729&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Layer Reachability&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;233&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;261,615&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;4,294,901,729&lt;/td&gt;
    &lt;/tr&gt;

    &lt;tr&gt;
      &lt;th align=&quot;right&quot; valign=&quot;top&quot; rowspan=&quot;2&quot;&gt;32&lt;/th&gt;
      &lt;td&gt;Baseline&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;537&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;1,933,425&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;152,544,843,873&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Layer Reachability&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;529&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;1,933,407&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;152,544,843,841&lt;/td&gt;
    &lt;/tr&gt;

    &lt;tr&gt;
      &lt;th align=&quot;right&quot; valign=&quot;top&quot; rowspan=&quot;2&quot;&gt;64&lt;/th&gt;
      &lt;td&gt;Baseline&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;1,033&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;9,815,535&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;2,816,814,940,129&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Layer Reachability&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;905&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;9,814,437&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;2,816,814,934,817&lt;/td&gt;
    &lt;/tr&gt;

    &lt;tr&gt;
      &lt;th align=&quot;right&quot; valign=&quot;top&quot; rowspan=&quot;2&quot;&gt;128&lt;/th&gt;
      &lt;td&gt;Baseline&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;1,769&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;38,400,465&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;33,080,342,678,945&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Layer Reachability&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;905&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;38,369,571&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;33,080,342,314,753&lt;/td&gt;
    &lt;/tr&gt;

    &lt;tr&gt;
      &lt;th align=&quot;right&quot; valign=&quot;top&quot; rowspan=&quot;2&quot;&gt;256&lt;/th&gt;
      &lt;td&gt;Baseline&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;2,793&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;124,140,015&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;278,653,866,803,169&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Layer Reachability&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;905&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;123,560,373&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;278,653,849,430,401&lt;/td&gt;
    &lt;/tr&gt;

    &lt;tr&gt;
      &lt;th align=&quot;right&quot; valign=&quot;top&quot; rowspan=&quot;2&quot;&gt;512&lt;/th&gt;
      &lt;td&gt;Baseline&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;4,153&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;347,066,865&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;1,819,787,258,282,209&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Layer Reachability&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;905&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;339,166,485&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;1,819,786,604,950,209&lt;/td&gt;
    &lt;/tr&gt;

    &lt;tr&gt;
      &lt;th align=&quot;right&quot; valign=&quot;top&quot; rowspan=&quot;2&quot;&gt;1024&lt;/th&gt;
      &lt;td&gt;Baseline&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;5,897&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;865,782,255&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;9,718,525,023,289,313&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Layer Reachability&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;905&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;786,513,819&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;9,718,504,608,259,073&lt;/td&gt;
    &lt;/tr&gt;

    &lt;tr&gt;
      &lt;th align=&quot;right&quot; valign=&quot;top&quot; rowspan=&quot;2&quot;&gt;2048&lt;/th&gt;
      &lt;td&gt;Baseline&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;8,073&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;1,970,527,185&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;44,096,709,674,720,289&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Layer Reachability&lt;/td&gt;
      &lt;td align=&quot;right&quot;&gt;905&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;1,400,665,575&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;44,096,167,159,459,777&lt;/td&gt;
    &lt;/tr&gt;

  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;This has a large effect on the 2x2 board, reducing the number of states from 8,073 to 905 for the game up to 2048, and it’s notable that the figure for the number of reachable states does not increase from 905 for maximum tiles over &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;32&lt;/code&gt;, because it’s not possible to reach tiles larger than &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;32&lt;/code&gt; on a 2x2 board. It also has some effect on the 3x3 board, but on the 4x4 board, there is relatively little effect — we remove “only” about 500 billion states from the total for the game to 2048.&lt;/p&gt;

&lt;p&gt;In graphical form, these data look like:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
&lt;img src=&quot;/assets/2048/combinatorics_totals.svg&quot; alt=&quot;Estimated number of states each for board size and maximum tile&quot; /&gt;
&lt;/p&gt;

&lt;h1 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h1&gt;

&lt;p&gt;We’ve obtained some rough estimates for the number of states in the game of 2048 and similar games on smaller boards and to lesser tiles. Our best estimate so far for the number of states in the 4x4 game to 2048 is roughly 44 quadrillion (~\(10^{16}\)).&lt;/p&gt;

&lt;p&gt;It is likely that this and the other estimates are substantial overestimates, because there are many reasons that states might be counted here but still not be reachable in the game. For example, a state like the one in the cover image for this blog post:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
&lt;img src=&quot;/assets/2048/2048_infeasible_board.png&quot; alt=&quot;An infeasible board position with three 2 tiles in the middle with empty cells around&quot; style=&quot;max-width: 10em;&quot; /&gt;
&lt;/p&gt;

&lt;p&gt;satisfies all of the restrictions we’ve considered here, but it is still not possible to reach it, because we must have swiped in some direction before getting to this state, and that would have moved two of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt; tiles to the edge of the board. It may be possible to adapt the counting arguments above to take this (and likely other restrictions) into account, but I have not figured out how!&lt;/p&gt;

&lt;p&gt;In &lt;a href=&quot;/articles/2017/12/10/counting-states-enumeration-2048.html&quot;&gt;the next post&lt;/a&gt;, we’ll see that the number of actually reachable states is much lower by actually enumerating them. There will still be a lot of them for the 3x3 and 4x4 boards, so we will need some computer science as well as mathematics.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt; &lt;/p&gt;

&lt;p&gt;If you’ve read this far, perhaps you should &lt;a href=&quot;https://twitter.com/jdleesmiller&quot;&gt;follow me on twitter&lt;/a&gt;, or even apply to work at &lt;a href=&quot;https://www.overleaf.com/jobs&quot;&gt;Overleaf&lt;/a&gt;. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:)&lt;/code&gt;&lt;/p&gt;

&lt;h1 id=&quot;footnotes&quot;&gt;Footnotes&lt;/h1&gt;

&lt;script type=&quot;text/x-mathjax-config&quot;&gt;
MathJax.Hub.Config({
  TeX: { equationNumbers: { autoNumber: &quot;AMS&quot; } }
});
&lt;/script&gt;

&lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML&quot; type=&quot;text/javascript&quot;&gt;&lt;/script&gt;

&lt;!--
To make the infeasible state diagram:

Go to http://gabrielecirulli.github.io/2048/
Open dev console
gm = new GameManager(4, KeyboardInputManager, HTMLActuator, LocalStorageManager)
// Remove the two random tiles (if needed); for me they were:
gm.grid.removeTile(new Tile({x: 0, y: 2}))
gm.grid.removeTile(new Tile({x: 3, y: 3}))
gm.actuate()
// Board should be empty
gm.grid.insertTile(new Tile({x: 1, y: 1}, 2))
gm.grid.insertTile(new Tile({x: 2, y: 1}, 2))
gm.grid.insertTile(new Tile({x: 1, y: 2}, 2))
gm.actuate()
// Board should have the three faked tiles.
--&gt;
&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:Chinn&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;Chinn, P. and Niederhausen, H., 2004. Compositions into powers of 2. &lt;em&gt;Congressus Numerantium&lt;/em&gt;, 168, p.215. &lt;a href=&quot;http://math.fau.edu/Niederhausen/HTML/Papers/CompositionsIntoPowersOf2.doc&quot;&gt;(preprint)&lt;/a&gt; &lt;a href=&quot;#fnref:Chinn&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:smallest-board&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;We could also have shown this using the Markov chain analysis from &lt;a href=&quot;/articles/2017/08/05/markov-chain-2048.html&quot;&gt;my previous post&lt;/a&gt; by removing all of the states with more than nine tiles, and seeing whether it was still possible to reach the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2048&lt;/code&gt; tile. If we &lt;a href=&quot;https://github.com/jdleesmiller/twenty48/blob/4337c357f2cc14bdc3e14ddaa5207ad2a6a972e6/bin/markov_chain#L382-L397&quot;&gt;do this&lt;/a&gt;, we find that &lt;a href=&quot;https://github.com/jdleesmiller/twenty48/blob/4337c357f2cc14bdc3e14ddaa5207ad2a6a972e6/data/markov_chain/minmax_cells.csv&quot;&gt;it is not&lt;/a&gt;. Interestingly, if we allow the Markov chain to continue to larger maximum tile values, the same analysis shows that it is possible to reach the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;131072&lt;/code&gt; (that is, \(2^{17}\)) tile on a 4x4 board, if the structure of the board is ignored. Whether this is true when there are structural constraints is still open, but I suspect it is not. It is, however, possible to play to at least the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;8192&lt;/code&gt; tile, as shown by &lt;a href=&quot;https://www.youtube.com/watch?v=96ab_dK6JM0&quot;&gt;this AI bot&lt;/a&gt;, and there is a &lt;a href=&quot;https://www.youtube.com/watch?v=MDkZkweB5lM&quot;&gt;‘proof of concept’ video&lt;/a&gt; for the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;131072&lt;/code&gt; tile. &lt;a href=&quot;#fnref:smallest-board&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</description>
        <pubDate>Sun, 17 Sep 2017 01:00:00 +0000</pubDate>
        <link>http://jdlm.info/articles/2017/09/17/counting-states-combinatorics-2048.html</link>
        <guid isPermaLink="true">http://jdlm.info/articles/2017/09/17/counting-states-combinatorics-2048.html</guid>
        
        
        <category>articles</category>
        
      </item>
    
      <item>
        <title>The Mathematics of 2048: Minimum Moves to Win with Markov Chains</title>
        <description>&lt;p&gt;&lt;strong&gt;Updates&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2017-09-25&lt;/strong&gt; There was some lively &lt;a href=&quot;https://news.ycombinator.com/item?id=15327837&quot;&gt;discussion about this series on Hacker News&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post is the first in a series. Next: &lt;a href=&quot;/articles/2017/09/17/counting-states-combinatorics-2048.html&quot;&gt;Counting States with Combinatorics&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt; &lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/2048/2048.png&quot; alt=&quot;Screenshot of 2048&quot; style=&quot;width: 40%; float: right; margin-left: 10pt; border: 6pt solid #eee;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;As part of a recent revamp, &lt;a href=&quot;http://gabrielecirulli.github.io/2048&quot;&gt;2048&lt;/a&gt;’s “You win!” screen started reporting the number of moves it took to win, which made me wonder: how many moves should it take to win?&lt;/p&gt;

&lt;p&gt;In this post, we’ll answer that question by modeling the game of 2048 as a Markov chain and analyzing it to show that, no matter how well the player plays, &lt;strong&gt;the number of moves required to win the game is at least 938.8 on average&lt;/strong&gt;. This gives us a benchmark — if you can win in around this number of moves, you’re doing pretty well.&lt;/p&gt;

&lt;p&gt;The number of moves needed to win depends on random chance, because the game adds &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt; tiles at random. The analysis will also show that the distribution for the minimum number of moves to win has a standard deviation of 8.3 moves, and that its overall shape is well-approximated by a mixture of binomial distributions.&lt;/p&gt;

&lt;p&gt;To obtain these results, we’ll use a simplified version of 2048: instead of placing the tiles on a board, we’ll… throw them into a bag. That is, we’ll ignore the geometric constraints imposed by the board on which tiles can be merged together. This simplification makes our job much easier, because the player no longer has to make any decisions &lt;sup id=&quot;fnref:mdp&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:mdp&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;, and because we don’t have to keep track of where the tiles are on the board.&lt;/p&gt;

&lt;p&gt;The price we pay for relaxing the geometric constraints is that we can only compute a lower bound on the expected number of moves to win; it might be that the geometric constraints make it impossible to attain that bound. However, by playing many games of 2048 (for science!), I’ll also show that we can often get close to this lower bound in practice.&lt;/p&gt;

&lt;p&gt;If you’re not familiar with 2048 or with Markov chains, that’s OK — I’ll introduce the necessary ideas as we go. The (research quality) code behind this article is &lt;a href=&quot;https://github.com/jdleesmiller/twenty48&quot;&gt;open source&lt;/a&gt;, in case you’d like to see the code to generate &lt;a href=&quot;https://github.com/jdleesmiller/twenty48/blob/27c30f5e42861c87a2162efed38003a9db9e8b29/bin/markov_chain&quot;&gt;the chain&lt;/a&gt; and &lt;a href=&quot;https://github.com/jdleesmiller/twenty48/blob/27c30f5e42861c87a2162efed38003a9db9e8b29/data/markov_chain/plot.R&quot;&gt;the plots&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;2048-as-a-markov-chain&quot;&gt;2048 as a Markov Chain&lt;/h2&gt;

&lt;p&gt;To represent our simplified ‘2048 in a bag’ game as a Markov chain, we need to define the &lt;em&gt;states&lt;/em&gt; and the &lt;em&gt;transition probabilities&lt;/em&gt; of the chain. Each state is like a snapshot of the game at a moment in time, and the transition probabilities specify, for each state, which state is likely to come next.&lt;/p&gt;

&lt;p&gt;Here we will define each state to encode which tiles are currently in the bag. We don’t care about the order of the tiles, so we can think of it as a multiset of tiles. Initially, there are no tiles, so our initial state is simply the empty set. In diagram form, which we’ll add to below, this initial state looks like:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
&lt;img src=&quot;/assets/2048/markov_chain_initial_state.svg&quot; alt=&quot;The initial state for the Markov chain&quot; /&gt;
&lt;/p&gt;

&lt;h3 id=&quot;setting-up-the-board&quot;&gt;Setting up the Board&lt;/h3&gt;

&lt;p align=&quot;center&quot;&gt;
&lt;img src=&quot;/assets/2048/initial_states.png&quot; alt=&quot;Montage of a sample of eight setup states.&quot; style=&quot;height: 200px;&quot; /&gt;&lt;br /&gt;
A sample of a dozen new game boards.
&lt;/p&gt;

&lt;p&gt;When we start a new game of 2048, the game places two random tiles on the board (see examples above). To represent this in the Markov chain, we need to work out the transition probabilities from the initial state to each of the possible successor states.&lt;/p&gt;

&lt;p&gt;Fortunately, we can look at &lt;a href=&quot;https://github.com/gabrielecirulli/2048&quot;&gt;the game’s source code&lt;/a&gt; to find out how the game does this. Whenever the game places a random tile on the board, it always follows the same process: &lt;em&gt;pick an available cell uniformly at random, then add a new tile either with value &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt;, with probability 0.9, or value &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt;, with probability 0.1&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;For 2048 in a bag, we don’t care about finding an available cell, because we haven’t put any capacity constraint on the bag; we just care about adding either a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt; tile with the given probabilities. This leads to three possible successor states for the initial state:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;\(\{2, 2\}\) when both of the new tiles are &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt;s. This happens with probability \(0.9 \times 0.9 = 0.81\).&lt;/li&gt;
  &lt;li&gt;\(\{4, 4\}\) when both of the new tiles are &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt;s. This happens with probability \(0.1 \times 0.1 = 0.01\) — that is, you are pretty lucky if you start with two &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt;s.&lt;/li&gt;
  &lt;li&gt;\(\{2, 4\}\) when the new tiles are &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt; and then &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt;, which happens with probability \(0.9 \times 0.1 = 0.09\), or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt; and then &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt;, which happens with probability \(0.1 \times 0.9 = 0.09\). We don’t care about order, so both cases lead to the same state, with total probability \(0.09 + 0.09 = 0.18\).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We can add these successor states and their transition probabilities to the Markov chain diagram as follows, where the transition probabilities are written on the edge labels, and the new nodes and edges are shown in blue:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
&lt;img src=&quot;/assets/2048/markov_chain_0.svg&quot; alt=&quot;Directed graph showing the initial state and its immediate successors: {2, 2}, {2, 4}, {4, 4}&quot; /&gt;
&lt;/p&gt;

&lt;h3 id=&quot;playing-the-game&quot;&gt;Playing the Game&lt;/h3&gt;

&lt;p&gt;With the first pair of tiles now placed, we’re ready to start playing the game. In the real game, this means that the player gets to swipe left, right, up or down to try to bring pairs of like tiles together. In the bag game, however, there is nothing to stop us from merging all pairs of like tiles, so that’s what we’ll do.&lt;/p&gt;

&lt;p&gt;In particular, the rule for merging tiles in the bag game is: &lt;em&gt;find all of the pairs of tiles in the bag that have the same value and remove them; then replace each pair of tiles with a single tile with twice the value&lt;/em&gt; &lt;sup id=&quot;fnref:merge&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:merge&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;

&lt;p&gt;Once pairs of like tiles have been merged, the game adds a single new tile at random using the same process as above — that is, a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt; tile with probability 0.9, or a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt; tile with probability 0.1 — to arrive at the successor state.&lt;/p&gt;

&lt;p&gt;For example, to find the possible successors of the state \(\{2, 2\}\), we first merge the two &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt; tiles into a single &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt; tile, and then the game will add either a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt; tile or a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt; tile. The possible successors are therefore \(\{2, 4\}\) and \(\{4, 4\}\), which, as it happens, we have already encountered. The diagram including these two transitions from \(\{2, 2\}\), which have probability 0.9 and 0.1 respectively, is then:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
&lt;img src=&quot;/assets/2048/markov_chain_1.svg&quot; alt=&quot;Directed graph showing the additional transitions from the {2, 2} state&quot; /&gt;
&lt;/p&gt;

&lt;p&gt;If we follow the same process for the successors of \(\{2, 4\}\), we see that no merging is possible yet, because there is no pair of like tiles, and the successor state will either be \(\{2, 2, 4\}\) or \(\{2, 4, 4\}\), depending on whether the new tile is a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt;. The updated diagram is then:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
&lt;img src=&quot;/assets/2048/markov_chain_2.svg&quot; alt=&quot;Directed graph showing the additional transitions from the {2, 4} state&quot; /&gt;
&lt;/p&gt;

&lt;h3 id=&quot;layers-and-skipping&quot;&gt;Layers and ‘Skipping’&lt;/h3&gt;

&lt;p&gt;We can continue adding transitions in this way. However, as we add more states and transitions, the diagrams can become quite complicated &lt;sup id=&quot;fnref:dot&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:dot&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;. We can make the diagrams a bit more orderly by using the following observation: &lt;strong&gt;the sum of the tiles in the bag increases by either 2 or 4 with each transition&lt;/strong&gt;. This is because merging pairs of like tiles does not change the sum of the tiles in the bag (or on the board — this property also holds in the real game), and the game always adds either a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt; tile or a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt; tile.&lt;/p&gt;

&lt;p&gt;If we group states together into ‘layers’ by their sum, the first few layers look like this:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
&lt;img src=&quot;/assets/2048/markov_chain_3.svg&quot; alt=&quot;Directed graph showing states up to sum 12&quot; /&gt;
&lt;/p&gt;

&lt;p&gt;For later layers, I’ve also omitted the labels for transitions with probability 0.9 (solid lines, unless otherwise labelled) and 0.1 (dashed lines), to reduce clutter.&lt;/p&gt;

&lt;p&gt;Grouping the states into layers by sum makes another pattern clear: each transition (other than those from the initial state) is either to the next layer, with probability 0.9, or the layer after, with probability 0.1. (This is particularly clear if you look at the layers with sums 8, 10 and 12 in the diagram above.) That is, most of the time the game gives us a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt;, and we’ll transition to the next layer, but sometimes we get lucky, and the game gives us a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt;, which means we get to skip a layer, getting us slightly closer to our goal of reaching the 2048 tile.&lt;/p&gt;

&lt;h3 id=&quot;the-end-game&quot;&gt;The End Game&lt;/h3&gt;

&lt;p&gt;We could continue this process forever, but since we are only interested in reaching the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2048&lt;/code&gt; tile, we’ll stop it at that point by making any state with a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2048&lt;/code&gt; tile an &lt;em&gt;absorbing&lt;/em&gt; state. An absorbing state has a single transition, which is to itself with probability 1 — that is, once you reach an absorbing state, you can never leave.&lt;/p&gt;

&lt;p&gt;In this case all of the absorbing states are ‘win states’ — you have a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2048&lt;/code&gt; tile and have therefore won the game. There is no way to ‘lose’ the bag game, because unlike in the real game, we cannot get into a situation where the board (or bag) is full.&lt;/p&gt;

&lt;p&gt;The first state that contains a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2048&lt;/code&gt; tile is in the layer with sum 2066. It’s notable that you can’t get a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2048&lt;/code&gt; tile on its own on the board — it takes a few moves to merge the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1024&lt;/code&gt; tiles, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;512&lt;/code&gt; tiles, and so on, during which you accumulate more &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt; tiles. This is why the sum of the tiles for the first state with a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2048&lt;/code&gt; tile is higher than 2048.&lt;/p&gt;

&lt;p&gt;Here’s what the graph looks like around this first winning state (&lt;a href=&quot;/assets/2048/markov_chain_end.svg&quot;&gt;see more&lt;/a&gt;), with the absorbing states colored red:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
&lt;a href=&quot;/assets/2048/markov_chain_end.svg&quot;&gt;&lt;img src=&quot;/assets/2048/markov_chain_end_screenshot.png&quot; alt=&quot;Screenshot of the end of the Markov chain&quot; style=&quot;border: 1px dotted grey; margin: 10px;&quot; /&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;If we continue to add transitions until there are no non-absorbing states left, we eventually end up with 3487 states, of which 26 are absorbing; this completes the definition of the Markov chain. The diagram for the full chain is quite large, but if your device can handle a 5MB SVG file, &lt;a href=&quot;/assets/2048/markov_chain_big.svg&quot;&gt;here is a diagram of the full chain&lt;/a&gt; (you may need to scroll down a bit to see the start). When very zoomed out, it looks like this:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
&lt;a href=&quot;/assets/2048/markov_chain_big.svg&quot;&gt;&lt;img src=&quot;/assets/2048/markov_chain_big_thumbnail.png&quot; alt=&quot;Zoomed out view of the whole chain&quot; style=&quot;border: 0;&quot; /&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;h3 id=&quot;sampling-from-the-chain&quot;&gt;Sampling from the Chain&lt;/h3&gt;

&lt;p&gt;Now that we have put in the effort to model 2048 (in a bag) as a Markov chain, the simplest way to find out how many moves it takes before we are absorbed is to run simulations. In each simulation, we generate a single trajectory through the chain by starting at the initial state, then choosing a successor state at random in proportion to the transition probabilities, then repeating from that state. After one million simulation runs, the following distribution emerges for the number of moves to win:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
&lt;img src=&quot;/assets/2048/markov_chain_moves_histogram.svg&quot; alt=&quot;Distribution of the number of moves to win, with the mean of 938.8 highlighted&quot; /&gt;
&lt;/p&gt;

&lt;p&gt;The mean, which is marked by the vertical blue line, comes out at &lt;strong&gt;938.8 moves&lt;/strong&gt;, excluding the first transition from the initial state, with a standard deviation of &lt;strong&gt;8.3 moves&lt;/strong&gt;. So, that’s our answer for the minimum expected number of moves to win the game!&lt;/p&gt;

&lt;p&gt;The theory of Markov chains also lets us calculate some of these properties directly using clever mathematics. In &lt;a href=&quot;#appendix-a-analysis-of-the-markov-chain&quot;&gt;Appendix A&lt;/a&gt;, I’ll show how to calculate the mean and standard deviation for the number of moves without relying on simulation. Then, in &lt;a href=&quot;#appendix-b-the-shape-of-the-distribution&quot;&gt;Appendix B&lt;/a&gt;, I’ll use some of properties of the chain to offer at least a partial explanation of the shape of the distribution.&lt;/p&gt;

&lt;h2 id=&quot;putting-theory-to-the-test&quot;&gt;Putting Theory to the Test&lt;/h2&gt;

&lt;p&gt;Finally, to test these results in real life, I played a lot of 2048 (for science!) and, for the 28 games I won, recorded the number of moves it took and also the sum of the tiles on the board when I reached the 2048 tile &lt;sup id=&quot;fnref:changes&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:changes&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;4&lt;/a&gt;&lt;/sup&gt;:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
&lt;img src=&quot;/assets/2048/wins.png&quot; alt=&quot;Montage of 28 winning games of 2048&quot; /&gt;
&lt;/p&gt;

&lt;p&gt;Transcribing these numbers into a spreadsheet and plotting them leads to the following scatter plot:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
&lt;img src=&quot;/assets/2048/markov_chain_human.svg&quot; alt=&quot;Moves to Win and Tiles on Board for the 28 games I won&quot; /&gt;
&lt;/p&gt;

&lt;p&gt;I’ve marked the minimum expected number of moves, plus or minus one standard deviation, in blue, and the tile sum 2066, which we found to be the lowest sum of tiles for which it was possible to have a 2048 tile, in red.&lt;/p&gt;

&lt;p&gt;The sum of the tiles on the board is important, because when it’s large, that typically means that I made a mistake that left a large tile stranded somewhere I could not merge it with any other tile. It then took many more moves to build up that tile again in a place where it could be merged (or to set up the board to try to get into the right place to merge) with another large tile.&lt;/p&gt;

&lt;p&gt;If I were very good at playing 2048, we’d predict my results would cluster in the bottom left corner of the graph, and that most of them would lie between the dashed blue lines. In fact, we see that, while I sometimes get close to this ideal, I am not very consistent — there are plenty of points in the top right, with lots of extra moves and extra tiles.&lt;/p&gt;

&lt;p&gt;This plot also highlights the fact that this analysis gives us only the minimum &lt;em&gt;expected&lt;/em&gt; number of moves. There were a few games where I got lucky and won in less than 938.8 moves, including one win with 927 moves and a tile sum of 2076. (It is the second from the left in the bottom row of the montage above.) This is essentially because I got a lot of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt; tiles in that game, just by chance, and also because I didn’t make any major blunders that required extra moves.&lt;/p&gt;

&lt;p&gt;In principle, there is non-zero probability that we could win the game in only 519 moves. We can find this by walking through the chain, always taking the transition for the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt;, and counting the number of transitions required to reach a 2048 tile. However, the probability of this occurring is \(0.1^{521}\), or \(10^{-521}\); there are only about \(10^{80}\) atoms in the observable universe, so you shouldn’t hold your breath waiting for such a game to happen to you. Similarly, if we are very unlucky and always get &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt; tiles, we should still be able to win in only 1032 moves. Such a game is much more likely, with a probability of \(0.9^{1034}\), which is about \(10^{-48}\), but you probably shouldn’t hold your breath waiting for that game either. The average of 938.8 moves is much closer to 1032 than 519, because &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt; tiles are much more likely than &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt; tiles.&lt;/p&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;In this post we have seen how to construct a Markov chain that models how a game of 2048 evolves if it is always possible to merge like tiles. By doing so, we’ve been able to apply techniques from the theory of absorbing Markov chains to calculate interesting properties of the game, and in particular that it takes at least 938.8 moves to win, on average.&lt;/p&gt;

&lt;p&gt;The main simplification that enabled this approach was to ignore the structure of the board, effectively assuming that we threw tiles into a bag, rather than placing them onto the board. In &lt;a href=&quot;/articles/2017/09/17/counting-states-combinatorics-2048.html&quot;&gt;my next post&lt;/a&gt;, I plan to look at what happens when we do consider the structure of the board. We’ll see that the number of states we need to consider becomes many orders of magnitude larger (though perhaps not as large as one might think), and also that we will need to leave the world of Markov chains and enter the world of Markov Decision Processes, which allow us to bring the player back into the equation, and in principle may allow us to ‘solve’ the game completely — to find a provably optimal way of playing.&lt;/p&gt;

&lt;h2 id=&quot;appendix-a-analysis-of-the-markov-chain&quot;&gt;Appendix A: Analysis of the Markov Chain&lt;/h2&gt;

&lt;p&gt;Once that we’ve defined our Markov chain, we can bring some powerful mathematical machinery to bear to calculate its properties without simulation. Many of these calculations are possible only because our Markov chain is a special type of Markov chain called an &lt;a href=&quot;https://en.wikipedia.org/wiki/Absorbing_Markov_chain&quot;&gt;absorbing Markov chain&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The criteria for being an absorbing Markov chain are that:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;There must be at least one absorbing state. As we’ve seen above, there are 26 absorbing states, one for each winning state with a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2048&lt;/code&gt; tile.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;For any state, it is possible to reach an absorbing state in a finite number of transitions. One way to see that this holds for our chain is to observe that there are no loops other than for the absorbing states — except for the absorbing states, the chain is a directed acyclic graph.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;the-transition-matrix&quot;&gt;The Transition Matrix&lt;/h3&gt;

&lt;p&gt;Now that we have established that we have an absorbing Markov chain, the next step is to write out its &lt;em&gt;transition matrix&lt;/em&gt; in &lt;em&gt;canonical form&lt;/em&gt;. A transition matrix is a matrix that organizes the transition probabilities, which we defined for our chain above, such that the \((i, j)\) entry is the probability of transitioning from state \(i\) to state \(j\).&lt;/p&gt;

&lt;p&gt;For the transition matrix, \(\mathbf{P}\), of an absorbing chain with \(r\) absorbing states and \(t\) &lt;em&gt;transient&lt;/em&gt; (which means non-absorbing) states, to be in canonical form, it must be possible to break it up into four smaller matrices, \(\mathbf{Q}\), \(\mathbf{R}\), \(\mathbf{0}\) and \(\mathbf{I}_r\), such that:
\[
\mathbf{P} = \left(
\begin{array}{cc}
 \mathbf{Q} &amp;amp; \mathbf{R} \\\&lt;br /&gt;
 \mathbf{0} &amp;amp; \mathbf{I}_r
\end{array}
\right)
\]
where \(\mathbf{Q}\) is a \(t \times t\) matrix that describes the probability of transitioning from one transient state to another transient state, \(\mathbf{R}\) is a \(t \times r\) matrix that describes the probability of transitioning from a transient state to an absorbing state, \(\mathbf{0}\) denotes an \(r \times t\) matrix of zeros, and \(\mathbf{I}_r\) is the transition matrix for the absorbing states, which is an \(r \times r\) identity matrix.&lt;/p&gt;

&lt;p&gt;To get a transition matrix in canonical form for our chain, we need to decide on an ordering of the states. It suffices to order states (1) by whether they are absorbing, with absorbing states last, then (2) by the sum of their tiles, in ascending order, and finally (3) in lexical order, to break ties. If we do this, we obtain the following matrix:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
&lt;img src=&quot;/assets/2048/markov_chain_canonical.svg&quot; alt=&quot;The full transition matrix for the absorbing Markov chain&quot; /&gt;
&lt;/p&gt;

&lt;p&gt;It’s quite large, namely \(3487 \times 3487\), so when zoomed out it just looks pretty much diagonal, but if we zoom in on the lower right hand corner, we can see that it does have some structure, and in particular it has the canonical form that we’re after:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
&lt;img src=&quot;/assets/2048/markov_chain_canonical_lower_right.svg&quot; alt=&quot;The lower right hand corner of the transition matrix for the absorbing Markov chain, which shows more structure&quot; /&gt;
&lt;/p&gt;

&lt;h3 id=&quot;the-fundamental-matrix&quot;&gt;The Fundamental Matrix&lt;/h3&gt;

&lt;p&gt;With the transition matrix in canonical form, the next step is to use it to find what is called the &lt;em&gt;fundamental matrix&lt;/em&gt; for the chain, which will let us calculate the expected number of transitions before absorption, which is (finally!) the answer to our original question.&lt;/p&gt;

&lt;p&gt;The fundamental matrix, \(\mathbf{N}\), is defined in terms of \(\mathbf{Q}\) by the identity
\[
\mathbf{N} = \sum_{k=0}^{\infty} \mathbf{Q}^k
\]
where \(\mathbf{Q}^k\) denotes the \(k\)th matrix power of \(\mathbf{Q}\).&lt;/p&gt;

&lt;p&gt;The \((i, j)\) entry of \(\mathbf{N}\) has a particular interpretation: it is the expected number of times that we would enter state \(j\) if we followed the chain starting from state \(i\). To see this, we can we observe that, just as the \((i, j)\) entry of \(\mathbf{Q}\) is the probability of transitioning from state \(i\) to state \(j\) in a single transition, the \((i, j)\) entry of \(\mathbf{Q}^k\) is the probability of entering state \(j\) exactly \(k\) transitions after entering state \(i\). If, for a given pair of states \(i\) and \(j\), we add up said probabilities for all \(k \geq 0\), the summation includes every time at which we could possibly enter state \(j\) after state \(i\), weighted by the corresponding probability, which is what gives us the desired expectation.&lt;/p&gt;

&lt;p&gt;Fortunately, the fundamental matrix can also be calculated directly, without the awkward infinite summation, as the inverse of the matrix \(\mathbf{I}_t - \mathbf{Q}\), where \(\mathbf{I}_t\) is the \(t \times t\) identity matrix; that is, \(\mathbf{N} = (\mathbf{I}_t - \mathbf{Q})^{-1}\). (The proof of this identity is left as an exercise for the reader!)&lt;/p&gt;

&lt;h3 id=&quot;expected-moves-to-win&quot;&gt;Expected Moves to Win&lt;/h3&gt;

&lt;p&gt;Once we have the fundamental matrix, we can find the expected number of transitions from any state \(i\) to an absorbing state by summing up all of the entries in row \(i\) — in other words, the number of transitions before we reach an absorbing state is the total number of transitions that we spend in all of the transient states along the way.&lt;/p&gt;

&lt;p&gt;We can obtain these row sums for all states at once by calculating the matrix-vector product \(\mathbf{N} \mathbf{1}\), where \(\mathbf{1}\) denotes a column vector of \(t\) ones. Since \(\mathbf{N} = (\mathbf{I}_t - \mathbf{Q})^{-1}\), we can do this efficiently by solving the linear system of equations
\[
(\mathbf{I}_t - \mathbf{Q})\mathbf{t} = \mathbf{1}
\]
for \(\mathbf{t}\). The entry in \(\mathbf{t}\) that corresponds to the initial state (the empty set, \(\{\}\)) is the number of transitions. In this case, the number that comes out is 939.8. To finish up, we just need to subtract \(1\), because the transition from the initial state doesn’t count as a move. This gives our final answer as &lt;strong&gt;938.8 moves&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We can also obtain the &lt;a href=&quot;https://en.wikipedia.org/wiki/Absorbing_Markov_chain#Variance_on_number_of_steps&quot;&gt;variance&lt;/a&gt; for the minimum number of moves as
\(
2(\mathbf{N} - \mathbf{I}_t) \mathbf{t} - \mathbf{t} \circ \mathbf{t}
\),
where \(\circ\) denotes the &lt;a href=&quot;https://en.wikipedia.org/wiki/Hadamard_product_(matrices)&quot;&gt;Hadamard (elementwise) product&lt;/a&gt;. For the initial state, the variance comes out as 69.5, which gives a standard deviation of &lt;strong&gt;8.3 moves&lt;/strong&gt;.&lt;/p&gt;

&lt;h2 id=&quot;appendix-b-the-shape-of-the-distribution&quot;&gt;Appendix B: The Shape of the Distribution&lt;/h2&gt;

&lt;p&gt;Perhaps remarkably, we were able to calculate both the mean and the variance of the moves-to-win distribution using the fundamental matrix from the Markov chain. It would however be nice to have some insight into why the distribution is the shape that it is. The approach I’ll suggest here is only approximate, but it does match the empirical results from the simulation of the chain quite closely, and it provides some useful insights.&lt;/p&gt;

&lt;p&gt;We’ll begin by revisiting an observation that we made above: the sum of the tiles on the board increases by either 2 or 4 with each transition (other than the first transition from the initial state). If we were interested in hitting a specific sum for the tiles on the board, rather than hitting a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2048&lt;/code&gt; tile, then it’s relatively straightforward to calculate the required number of transitions using the binomial distribution, as we’ll see below.&lt;/p&gt;

&lt;p&gt;So, the next question is, which sum should we aim to hit? From the Markov chain analysis above, we determined that there are 26 absorbing (winning) states, and we’ve also seen that they are in different ‘sum layers’, so there isn’t a single target sum — there are several target sums. What we need to know is the probability of being absorbed in each state, which is called an &lt;em&gt;absorbing probability&lt;/em&gt;. We can then add up the absorbing probabilities for each of the absorbing states in a particular sum layer to find a probability of winning with a given target sum.&lt;/p&gt;

&lt;h3 id=&quot;absorbing-probabilities&quot;&gt;Absorbing Probabilities&lt;/h3&gt;

&lt;p&gt;Fortunately, the absorbing probabilities can also be found from the fundamental matrix. In particular, we can obtain them by solving the linear equations
\[
(\mathbf{I}_t - \mathbf{Q}) \mathbf{B} = \mathbf{R}
\]
for the \(t \times r\) matrix \(\mathbf{B}\), whose \((i, j)\) entry is the probability of being absorbed in state \(j\) when starting from state \(i\). As before, we are interested in the absorbing probabilities when we start from the initial state. Plotting out the absorbing probabilities, there are 15 absorbing states for which the probabilities are large enough to plot (at least \(10^{-3}\)):&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
&lt;img src=&quot;/assets/2048/markov_chain_absorbing_probabilities.svg&quot; alt=&quot;Absorbing probabilities for the Markov chain&quot; /&gt;
&lt;/p&gt;

&lt;p&gt;In particular, most games end in either the \(\{2,2,8,8,2048\}\) state, which has sum 2068, or the \(\{2,4,16,2048\}\) state, which has sum 2070. Summing up all of the absorbing states by layer sum gives the complete layer sum probabilities:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
&lt;img src=&quot;/assets/2048/markov_chain_sum_probabilities.svg&quot; alt=&quot;Total absorbing probabilities by sum of tiles&quot; /&gt;
&lt;/p&gt;

&lt;h3 id=&quot;binomial-probabilities&quot;&gt;Binomial Probabilities&lt;/h3&gt;

&lt;p&gt;Now that we have some sums to aim for, and we know how often we are aiming for each one, the next question is, how many moves does it take to hit a particular sum? As noted above, we can think of this in terms of the &lt;a href=&quot;https://en.wikipedia.org/wiki/Binomial_distribution&quot;&gt;Binomial distribution&lt;/a&gt;, which lets us calculate the probability of a given number of “successes” out of a given number of “trials”.&lt;/p&gt;

&lt;p&gt;In this case, we’ll consider a “trial” to be a move, and a “success” to be a move in which the game gives us a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt; tile; as we’ve seen above, this happens with probability 0.1. A “failure” here is a move in which the game gives us a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt; tile, which happens with probability 0.9.&lt;/p&gt;

&lt;p&gt;With this interpretation of successes, in order to hit a given sum \(S\) in \(M\) moves, we need \(\frac{S}{2} - M\) successes out of \(M\) moves. This is because each move counts at least \(2\) toward the sum, which contributes a total of \(2M\), and each success counts an additional \(2\) toward the sum, for a total contribution of \(2 \left(\frac{S}{2} - M\right) = S - 2M\); adding these contributions together leaves the desired sum, \(S\).&lt;/p&gt;

&lt;p&gt;The joint probability of obtaining a sum \(S\) in a number of moves \(M\) is then Binomial, and in particular
\[
\mathrm{Pr}(M=m, S=s) = B\left(\frac{s}{2} - m; m, 0.1\right)
\]
where \(B(k; n, p)\) is the probability mass function for the binomial distribution, which gives the probability of exactly \(k\) successes in \(n\) trials, where the probability of success is \(p\), namely
\[
B(k; n, p) = {n\choose k}p^k(1-p)^{n-k}
\]
where \(n \choose k\) denotes a &lt;a href=&quot;https://en.wikipedia.org/wiki/Binomial_coefficient&quot;&gt;binomial coefficient&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Now if we know the target sum \(S\) that we are aiming for, we can compute the conditional distribution of the number of moves given that sum, which is what we are interested in, from the joint distribution. That is, we can find \(\mathrm{Pr}(M | S)\) as
\[
\mathrm{Pr}(M | S) = \frac{\mathrm{Pr}(M, S)}{\mathrm{Pr}(S)}.
\]
where \(\mathrm{Pr}(S)\) is obtained from the joint distribution by summing out \(M\) for each possible sum \(S\). It’s worth noting that \(\mathrm{P}(S)\) is less than one for each possible sum, because there’s always a chance that the game ‘skips’ the sum by placing a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt; tile.&lt;/p&gt;

&lt;p&gt;With these conditional distributions for each target sum in hand, we can then add them up, weighted by the total absorbing probability for the target sum, to obtain the overall distribution. This gives a fairly good match to the distribution from the simulation:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;
&lt;img src=&quot;/assets/2048/markov_chain_weighted_mixture.svg&quot; alt=&quot;Simulated and binomial mixture model distributions for minimum moves to win&quot; /&gt;
&lt;/p&gt;

&lt;p&gt;Here the simulated distribution is shown with the grey bars, and the colored areas show each of the conditional distributions, which are stacked. Each conditional distribution is scaled according to the total absorbing probability for its sum, and also shifted a few moves, with larger sums requiring more moves on average.&lt;/p&gt;

&lt;p&gt;One interpretation of this result is that, if playing optimally, the number of moves to win is essentially determined by how quickly the player can get to a sum large enough to have a 2048 tile, which is in turn governed by the number of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt; tiles, which follows a binomial distribution.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt; &lt;/p&gt;

&lt;p&gt;Thanks to &lt;a href=&quot;https://twitter.com/h0peth0mas&quot;&gt;Hope Thomas&lt;/a&gt; and &lt;a href=&quot;https://natestemen.github.io/&quot;&gt;Nate Stemen&lt;/a&gt; for reviewing drafts of this article.&lt;/p&gt;

&lt;p&gt;If you’ve read this far, perhaps you should &lt;a href=&quot;https://twitter.com/jdleesmiller&quot;&gt;follow me on twitter&lt;/a&gt;, or even apply to work at &lt;a href=&quot;https://www.overleaf.com/jobs&quot;&gt;Overleaf&lt;/a&gt;. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:)&lt;/code&gt;&lt;/p&gt;

&lt;h2 id=&quot;footnotes&quot;&gt;Footnotes&lt;/h2&gt;

&lt;script type=&quot;text/x-mathjax-config&quot;&gt;
MathJax.Hub.Config({
  TeX: { equationNumbers: { autoNumber: &quot;AMS&quot; } }
});
&lt;/script&gt;

&lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML&quot; type=&quot;text/javascript&quot;&gt;&lt;/script&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:mdp&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;If we do allow the player to make decisions, we have a &lt;a href=&quot;https://en.wikipedia.org/wiki/Markov_decision_process&quot;&gt;Markov Decision Process&lt;/a&gt;, rather than a Markov chain. That will be the subject of a later blog post. &lt;a href=&quot;#fnref:mdp&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:merge&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;Merging pairs of like tiles in this way captures an important nuance of the merging logic in the real game: if you have, for example, four &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt; tiles in a row, and you swipe to merge them, the result is two &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt; tiles, not a single &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;8&lt;/code&gt; tile. That is, you can’t merge newly merged tiles on a single swipe. &lt;a href=&quot;#fnref:merge&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:dot&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;The diagrams here come from the excellent &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dot&lt;/code&gt; tool in &lt;a href=&quot;http://www.graphviz.org/&quot;&gt;graphviz&lt;/a&gt;. If we don’t give &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dot&lt;/code&gt; a hint by grouping the states together into layers by sum, laying out the full graph can take quite a while. &lt;a href=&quot;#fnref:dot&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:changes&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;The appearance of the game’s “You win!” screen changed several times over the months in which I collected this data. For the record, playing 2048 was not the only thing I did during these months. &lt;a href=&quot;#fnref:changes&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</description>
        <pubDate>Sat, 05 Aug 2017 09:00:00 +0000</pubDate>
        <link>http://jdlm.info/articles/2017/08/05/markov-chain-2048.html</link>
        <guid isPermaLink="true">http://jdlm.info/articles/2017/08/05/markov-chain-2048.html</guid>
        
        
        <category>articles</category>
        
      </item>
    
  </channel>
</rss>
