diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..ed2cf9f --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,17 @@ +--- +name: Test +on: [push] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + - name: Install dependencies + run: bundle install + - name: Run tests + run: bundle exec rspec diff --git a/.github/workflows/update_readme.yml b/.github/workflows/update_readme.yml new file mode 100644 index 0000000..9ffa59b --- /dev/null +++ b/.github/workflows/update_readme.yml @@ -0,0 +1,31 @@ +--- +name: Update README + +on: + workflow_dispatch: + schedule: + - cron: '0 11 * * *' + +jobs: + update: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + - name: Install dependencies + run: bundle install + - name: Update README + run: |- + bin/update_readme + cat profile/README.md + - name: Commit and push if changed + run: |- + git diff + git config --global user.email "actions@users.noreply.github.com" + git config --global user.name "README Bot" + git add -A + git commit -m "Updated content" || exit 0 + git push diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..180bf07 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.bundle +vendor diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..408069a --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +ruby-3.4.1 diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..b98e685 --- /dev/null +++ b/Gemfile @@ -0,0 +1,8 @@ +source "https://rubygems.org" + +gem "excon" +gem "feedjira" + +group :development, :test do + gem "rspec" +end diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..fd26a72 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,57 @@ +GEM + remote: https://rubygems.org/ + specs: + crass (1.0.6) + diff-lcs (1.5.1) + excon (1.2.2) + feedjira (3.2.3) + loofah (>= 2.3.1, < 3) + sax-machine (>= 1.0, < 2) + loofah (2.23.1) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mini_portile2 (2.8.8) + nokogiri (1.18.1) + mini_portile2 (~> 2.8.2) + racc (~> 1.4) + nokogiri (1.18.1-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.1-arm-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.1-arm64-darwin) + racc (~> 1.4) + nokogiri (1.18.1-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.18.1-x86_64-linux-gnu) + racc (~> 1.4) + racc (1.8.1) + rspec (3.13.0) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.2) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.3) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-support (3.13.2) + sax-machine (1.3.2) + +PLATFORMS + aarch64-linux + arm-linux + arm64-darwin + x86-linux + x86_64-darwin + x86_64-linux + +DEPENDENCIES + excon + feedjira + rspec + +BUNDLED WITH + 2.5.23 diff --git a/bin/update_readme b/bin/update_readme new file mode 100755 index 0000000..bfeff84 --- /dev/null +++ b/bin/update_readme @@ -0,0 +1,14 @@ +#!/usr/bin/env ruby + +require "bundler" + +Bundler.require + +$:.push File.expand_path("../lib", __dir__) + +require "github_readme" + +readme = Readme.new("profile/README.md") +blog = RssFeed.new(url: "https://feeds.feedburner.com/GiantRobotsSmashingIntoOtherGiantRobots") + +readme.update(section: "blog", items: blog.take(5)) diff --git a/lib/feed_item.rb b/lib/feed_item.rb new file mode 100644 index 0000000..a0ec145 --- /dev/null +++ b/lib/feed_item.rb @@ -0,0 +1,15 @@ +class FeedItem + attr_accessor :id, :title, :url, :updated_at, :created_at + + def initialize(id:, title:, url:, created_at:, updated_at:) + @id = id + @title = title + @url = url + @created_at = created_at + @updated_at = updated_at + end + + def to_readme_line + "[#{title}](#{url})\n" + end +end diff --git a/lib/github_readme.rb b/lib/github_readme.rb new file mode 100644 index 0000000..b3e2e49 --- /dev/null +++ b/lib/github_readme.rb @@ -0,0 +1,5 @@ +require "excon" + +require "feed_item" +require "rss_feed" +require "readme" diff --git a/lib/readme.rb b/lib/readme.rb new file mode 100644 index 0000000..2b2b0e0 --- /dev/null +++ b/lib/readme.rb @@ -0,0 +1,32 @@ +class Readme + def initialize(file_path) + @file_path = file_path + end + + def update(section:, items:) + content = items.map(&:to_readme_line).join("\n") + new_contents = insert(file_contents, section, content) + File.write(file_path, new_contents) + end + + private + + attr_reader :file_path + + def file_contents + File.read(file_path) + end + + def insert(document, marker, content) + replacement = <<~REPLACEMENT + + #{content} + + REPLACEMENT + + document.gsub( + /.*/m, + replacement.chomp + ) + end +end diff --git a/lib/rss_feed.rb b/lib/rss_feed.rb new file mode 100644 index 0000000..4afe61f --- /dev/null +++ b/lib/rss_feed.rb @@ -0,0 +1,36 @@ +require "json" +require "excon" +require "feedjira" + +class RssFeed + include Enumerable + + def initialize(url:) + @url = url + end + + def each + feed.entries.each do |entry| + yield FeedItem.new( + id: entry.id, + title: entry.title, + url: entry.url, + created_at: entry.published, + updated_at: entry.updated + ) + end + end + + private + + attr_reader :url + + def feed + @feed ||= + begin + client = Excon.new(url) + response = client.get + Feedjira.parse(response.body) + end + end +end diff --git a/profile/README.md b/profile/README.md index 3cafaa8..26cdf78 100644 --- a/profile/README.md +++ b/profile/README.md @@ -10,6 +10,15 @@ products at every stage of the product lifecycle. Since 2003, we have produced high-quality products for over [1,000 clients][3]. We are proud to leave every project with a stronger team and improved processes. +### Writing + + +[Why UX is more important than UI](https://thoughtbot.com/blog/why-ux-is-more-important-than-ui) + +[The digital nomad thoughtbottle](https://thoughtbot.com/blog/the-digital-nomad-thoughtbottle) + + + [1]: https://thoughtbot.com [2]: https://thoughtbot.com/services [3]: https://thoughtbot.com/case-studies diff --git a/spec/feed_item_spec.rb b/spec/feed_item_spec.rb new file mode 100644 index 0000000..f1acf2d --- /dev/null +++ b/spec/feed_item_spec.rb @@ -0,0 +1,20 @@ +require "spec_helper" +require "github_readme" + +RSpec.describe FeedItem do + describe "#to_readme_line" do + it "is a markdown link to the feed item" do + feed_item = described_class.new( + id: nil, + title: "Blog Post", + url: "http://example.com", + created_at: nil, + updated_at: nil, + ) + + expect(feed_item.to_readme_line).to eq( + "[Blog Post](http://example.com)\n", + ) + end + end +end diff --git a/spec/fixtures/rss_feed.xml b/spec/fixtures/rss_feed.xml new file mode 100644 index 0000000..adb869a --- /dev/null +++ b/spec/fixtures/rss_feed.xml @@ -0,0 +1,827 @@ + + + Giant Robots Smashing Into Other Giant Robots + Written by thoughtbot, your expert strategy, design, product management, and development partner. + + https://robots.thoughtbot.com/ + + + 2024-12-20T00:00:00+00:00 + + thoughtbot + + + Optimize your shell experience + + + Matheus Richard + + https://thoughtbot.com/blog/optimize-your-shell-experience + 2024-12-20T00:00:00+00:00 + 2024-12-19T22:40:12Z + <p>As developers we spend a fair amount of time in the shell. I believe we should +<a href="https://thoughtbot.com/blog/what-my-father-taught-me-about-software-development#master-your-tools">master our tools</a> as that&rsquo;ll make work easier.</p> + +<p>Instead of just telling you exactly what to do, I&rsquo;ll show you my process so you +can optimize your shell <em>for yourself, your work and your own enjoyment</em>.</p> +<h2 id="the-workflow"> + <a href="#the-workflow"> + The workflow + </a> +</h2> + +<p>I recommend checking what are your most used commands in the shell and start +optimizing from there. If you&rsquo;re using <a href="https://ohmyz.sh/">Oh my Zsh</a> you can +run the <code>zsh_stats</code> utility, otherwise try running this:</p> +<div class="highlight"><pre class="highlight shell"><code><span class="nb">history </span>1 | <span class="nb">awk</span> <span class="s1">'{CMD[$2]++;count++;}END { for (a in CMD)print CMD[a] " " CMD[a]/count*100 "% " a;}'</span> | <span class="nb">grep</span> <span class="nt">-v</span> <span class="s2">"./"</span> | column <span class="nt">-c3</span> <span class="nt">-s</span> <span class="s2">" "</span> <span class="nt">-t</span> | <span class="nb">sort</span> <span class="nt">-nr</span> | <span class="nb">nl</span> | <span class="nb">head</span> <span class="nt">-n10</span> +</code></pre></div> +<p>This script will check your <code>history</code> and display your most used commands. +Here&rsquo;s an example output:</p> +<div class="highlight"><pre class="highlight shell"><code>➜ ~ zsh_stats +37.7667% git +11.3858% vim +10.9372% ag +5.10469% <span class="nb">cd +</span>2.72183% rspec +2.71186% rake +2.56231% <span class="nb">ls +</span>1.78465% <span class="nb">rm +</span>1.47557% <span class="nb">mv +</span>1.13659% find +1.10668% <span class="nb">mkdir +</span>0.967099% bundle +</code></pre></div> +<p>Let&rsquo;s see how we could optimize our workflow based on that data.</p> +<h2 id="aliases"> + <a href="#aliases"> + Aliases + </a> +</h2> + +<p>Looks like <code>git</code> is the most used command, so optimizing it would be a big win +in productivity. That could mean using a helper like <a href="https://github.com/thoughtbot/gitsh"><code>gitsh</code></a> &ndash; an interactive +shell for git &ndash;, or just creating aliases for the most used git utilities like +<code>commit</code>, <code>push</code>, <code>pull</code>, etc.</p> +<div class="highlight"><pre class="highlight shell"><code><span class="nb">alias </span><span class="nv">gc</span><span class="o">=</span><span class="s2">"git commit --verbose"</span> +<span class="nb">alias </span><span class="nv">gp</span><span class="o">=</span><span class="s2">"git push"</span> +<span class="nb">alias </span><span class="nv">gprom</span><span class="o">=</span><span class="s2">"git pull --rebase origin main"</span> +</code></pre></div> +<p>That was my setup for a long time, but once I started using Oh my Zsh I just +went with <a href="https://github.com/ohmyzsh/ohmyzsh/blob/master/plugins/git/README.md">their git plugin</a>, which provides a lot of aliases and helpers for +git.</p> +<h2 id="less-typing-fewer-typos"> + <a href="#less-typing-fewer-typos"> + Less typing, fewer typos + </a> +</h2> + +<p>Another advantage of using aliases is that you can type less. This will not only +save you a few keystrokes, but also reduce the chance of typos. For example, +typing <code>bundle</code> or <code>bundle exec</code> is very tedious and there&rsquo;s a high chance that +I&rsquo;ll mistype it. So I created aliases for those:</p> +<div class="highlight"><pre class="highlight shell"><code><span class="nb">alias </span><span class="nv">b</span><span class="o">=</span><span class="s2">"bundle"</span> +<span class="nb">alias </span><span class="nv">be</span><span class="o">=</span><span class="s2">"bundle exec"</span> +</code></pre></div> +<p>You can customize this for your preferred tools, like <code>yarn</code> or <code>docker</code>.</p> + +<p>If you use Z shell, it has a feature that can auto correct your typos, if +you want to. <a href="https://blog.confirm.ch/zsh-tips-auto-completion-correction/">Here&rsquo;s how to enable it</a> and here&rsquo;s how it works:</p> +<div class="highlight"><pre class="highlight shell"><code><span class="nv">$ </span>cta README.md +zsh: correct <span class="s1">'cta'</span> to <span class="s1">'cat'</span> <span class="o">[</span>nyae]? y +<span class="c"># File content</span> +</code></pre></div><h2 id="accept-your-mistakes"> + <a href="#accept-your-mistakes"> + Accept your mistakes + </a> +</h2> + +<p>Even with aliases, the muscle memory sometimes is strong and you might still +keep using commands like <code>git</code> and mistype them. One trick that I like to do is +to create aliases for my common typos. For example, I often type <code>gti</code> instead +of <code>git</code>. After a lot of frustration, I gave up and accepted it. Instead of +forcing myself to obey the computer, I made the computer understand me:</p> +<div class="highlight"><pre class="highlight shell"><code><span class="nb">alias </span><span class="nv">gti</span><span class="o">=</span><span class="s2">"git"</span> +</code></pre></div> +<p>It&rsquo;s a simple thing, but it was such a relief for me! Remember, optimize +for <em>your</em> happiness! You can go further and use <a href="https://github.com/thoughtbot/dotfiles/blob/main/zsh/functions/g">a helper like <code>g</code></a> which is +basically impossible to type wrong now.</p> + +<aside class="info"> + <p>The opposite idea is something like + <a href="https://github.com/mtoyoda/sl">sl</a>, + where every time you typo <code>ls</code> a train will hit you. + </p> +</aside> +<h2 id="custom-made-helpers"> + <a href="#custom-made-helpers"> + Custom-made helpers + </a> +</h2> + +<p>The next step would be taking this further and creating custom-made helpers for +your own usage. For instance, I work on several Ruby projects. Some use Rubocop +as a linter, others use Standard. Besides that I also have a Rust pet project +that I play with from time to time. Instead of having to remember which linter +each project uses, I created a helper that figures that out for me:</p> +<div class="highlight"><pre class="highlight shell"><code><span class="k">function </span>lint<span class="o">()</span> <span class="o">{</span> + <span class="k">if </span><span class="nb">cat </span>Gemfile | <span class="nb">grep</span> <span class="s2">"standard"</span> <span class="o">&gt;</span>/dev/null 2&gt;/dev/null<span class="p">;</span> <span class="k">then + </span>be standardrb <span class="nv">$*</span> + <span class="k">elif </span><span class="nb">cat </span>Gemfile | <span class="nb">grep</span> <span class="s2">"rubocop"</span> <span class="o">&gt;</span>/dev/null 2&gt;/dev/null<span class="p">;</span> <span class="k">then + </span>be rubocop <span class="nv">$*</span> + <span class="k">elif</span> <span class="o">[[</span> <span class="nt">-a</span> <span class="s2">"Cargo.toml"</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then + </span>rustfmt <span class="k">**</span>/<span class="k">*</span>.rs + <span class="k">else + </span><span class="nb">echo</span> <span class="s2">"Linter not found"</span> + <span class="k">return </span>1 + <span class="k">fi</span> +<span class="o">}</span> +</code></pre></div> +<p>I could go further and create an abstraction for the arguments so <code>--fix</code> would +map to the appropriate flag for each linter. Another Ruby-related helper I have +is <code>puts</code>. Instead of opening <code>irb</code> to try something in Ruby, I just do it right +in the shell:</p> +<div class="highlight"><pre class="highlight shell"><code><span class="k">function </span>puts<span class="o">()</span> <span class="o">{</span> + ruby <span class="nt">-rdate</span> <span class="nt">-e</span> <span class="s2">"puts </span><span class="nv">$*</span><span class="s2">"</span> +<span class="o">}</span> + +<span class="c"># Usage</span> +puts Date.today - 10 <span class="c"># ten days ago</span> +</code></pre></div> +<aside class="info"> + <p>I went as far as creating + <a href="https://github.com/MatheusRich/matheus">a small gem</a> + with a list of helpers that I can easily install in any computer I&rsquo;m working on. + </p> +</aside> + +<p>These helpers can be as simple or as complex as you want. On the simpler side, +you could define one to create a directory and <code>cd</code> into it:</p> +<div class="highlight"><pre class="highlight shell"><code><span class="k">function </span>mcd<span class="o">()</span> <span class="o">{</span> + <span class="nb">mkdir</span> <span class="nt">-p</span> <span class="nv">$1</span> <span class="o">&amp;&amp;</span> <span class="nb">cd</span> <span class="nv">$1</span> +<span class="o">}</span> +</code></pre></div><h2 id="better-ux"> + <a href="#better-ux"> + Better UX + </a> +</h2> + +<p>The default shell experience can be very dry. Here are some things that you +might do to get a better experience.</p> +<h3 id="better-directory-navigation"> + <a href="#better-directory-navigation"> + Better directory navigation + </a> +</h3> + +<p>On our initial example, <code>cd</code> used to be one of the most used commands. There&rsquo;s a +few ways to improve that. You can simply set</p> +<div class="highlight"><pre class="highlight shell"><code>setopt autocd +</code></pre></div> +<p>Which will allow you to navigate directories without having to type <code>cd</code>. You +just type the directory name and hit enter.</p> + +<p>But I like to go further and use the <a href="https://github.com/agkozak/zsh-z">z plugin</a> in Z shell (also available +widely via <a href="https://github.com/ajeetdsouza/zoxide">zoxide</a>). These tools act like a smart <code>cd</code>. They rank the most +used paths and you can quickly jump to them by typing a few characters.</p> +<div class="highlight"><pre class="highlight shell"><code><span class="nv">$ </span><span class="nb">pwd</span> +/Users/matheus + +<span class="nv">$ </span>z thought +<span class="nv">$ </span><span class="nb">pwd</span> +/Users/matheus/Documents/dev/thoughtbot + +<span class="nv">$ </span>z down +<span class="nv">$ </span><span class="nb">pwd</span> +/Users/matheus/Downloads +</code></pre></div><h3 id="syntax-highlighting"> + <a href="#syntax-highlighting"> + Syntax highlighting + </a> +</h3> + +<p>I love syntax highlighting on my +code, and I like it in my shell as well. The Fish shell has this feature by +default, and I add it to my Z shell with the <a href="https://github.com/zsh-users/zsh-syntax-highlighting">zsh-syntax-highlighting</a> plugin.</p> + +<p>Here&rsquo;s what it looks like:</p> + +<figure> + <img src="https://images.thoughtbot.com/u0rdgubq85adoln05t4yuokz68d9_image.png" alt="Several shell prompts with different commands, some of them are highlighted in red to indicate they are invalid" /> + <figcaption>Invalid commands or syntax errors get highlighted in red</figcaption> +</figure> + +<p>It also highlights paths in your system, which is very useful when you&rsquo;re +dealing with files:</p> + +<figure> + <img src="https://images.thoughtbot.com/gcjnzjmbzcdklnie3p6bl7to6skx_image.png" alt="Two shell prompts usinc cd, but only the one with a valid path is hightlighted with an underscore" width=300 /> + <figcaption>Note that invalid paths do not have the underscore</figcaption> +</figure> + +<p>On the same note, I use <a href="https://github.com/sharkdp/bat">bat</a> as a replacement for <code>cat</code>, which provides syntax +highlighting to the output.</p> +<div class="highlight"><pre class="highlight shell"><code><span class="nb">alias cat</span><span class="o">=</span><span class="s1">'bat --paging=never'</span> +</code></pre></div> +<p>And I use <a href="https://github.com/dandavison/delta">delta</a> to add syntax highlighting to git diffs.</p> + +<aside class="info"> + <p>You can find other modern alternative for Unix commands in + <a href="https://github.com/ibraheemdev/modern-unix">this repository</a>. + </p> +</aside> +<h3 id="auto-completion"> + <a href="#auto-completion"> + Auto completion + </a> +</h3> + +<p>Also in the theme of typing less, instead of searching for a previous command +with <kdb>cmd</kdb>+<kdb>r</kdb> or using the arrow keys, I use +<a href="https://github.com/zsh-users/zsh-autosuggestions">zsh-autosuggestions</a>. It suggests commands based on your history as you type.</p> + +<figure> + <img src="https://images.thoughtbot.com/qlc5buxcvl1sbzxy1ujrp14cyjw0_image.png" alt="A shell prompt with the 'rake' command typed. After it, there is a suggestion 'test' in light gray" /> + <figcaption>Just press → and it will autocomplete your command</figcaption> +</figure> + +<p>I can hit <kdb>↑</kdb> to cycle through suggestions with the same prefix.</p> +<h2 id="closing-thoughts"> + <a href="#closing-thoughts"> + Closing thoughts + </a> +</h2> + +<p>I hope this inspires you to optimize your shell experience. You don&rsquo;t have to +implement everything at once, or everything at all. Pick what makes sense for +you and your workflow.</p> + +<p>Don&rsquo;t be afraid of using super custom aliases and tools. Most of the time you&rsquo;re +the only one using your shell, so it makes sense to optimize for your own needs. +Customize your aliases and tools just like you customize your color scheme.</p> + +<p>Some of these optimizations will take time to get used to, but it&rsquo;s worth it in +the long run. New things can be hard to adopt, so don&rsquo;t give up too soon!</p> + + Make your shell workflow as smooth as possible by creating helpers designed just for you. + true + + + What I learned about design by helping my grandfather send an email + + + Andrew Spencer + + https://thoughtbot.com/blog/what-i-learned-about-design-by-helping-my-grandfather-send-an-email + 2024-12-19T00:00:00+00:00 + 2024-12-19T17:15:00Z + <p>It started as a simple request, “Can you help me send an email to a friend.” Something most of us reading this have done countless times, but it was a new experience for my 89-year-old grandfather. He has always been pretty resistant to new technology. As a survey engineer, he drew site plans by hand long after most of his peers worked digitally. Today, he is retired and prefers newspapers and television over the internet and cell phones.</p> + +<p>He’s not the only person in this position. According to a 2018 National Center for Education Statistics study, <a href="https://nces.ed.gov/pubs2018/2018161.pdf">16% of US adults are not digitally literate</a>. International numbers are even higher at 23%. In the US, <a href="https://www.pewresearch.org/short-reads/2022/01/13/share-of-those-65-and-older-who-are-tech-users-has-grown-in-the-past-decade/">only 61% of adults over 65 own a cell phone</a>. It’s easy for those of us in tech to ignore these numbers and assume that everyone using our products is familiar with technology.</p> + +<p>As someone who uses digital technology every day for work, I have a strong natural bias toward designing and building products that cater to the technologically literate. This is a hard habit to break. Helping my grandfather send an email challenged some core assumptions I’ve made in my approach to design.</p> +<h2 id="an-easy-solution"> + <a href="#an-easy-solution"> + An easy solution? + </a> +</h2> + +<p>Typically, the answer to the problem of not understanding our users would be user testing so that we can build empathy and test our assumptions. In an ideal world, we’d test our products with a diverse audience that includes folks who are not digitally savvy. Still, this can be difficult in practice since that audience isn’t typically signing up for user testing platforms and digital research studies.</p> + +<p>When the ideal is not possible, we should do the second-best thing and educate ourselves so we can begin to build empathy. Even if we cannot test every user flow with a non-tech-friendly audience, we can still learn a lot about general patterns we should follow and apply to our work.</p> + +<p>Here’s what I learned about designing software for non-tech literate folks by observing my grandfather sending an email.</p> +<h2 id="affordances-are-important"> + <a href="#affordances-are-important"> + Affordances are important + </a> +</h2> + +<p>Design <a href="https://www.interaction-design.org/literature/topics/affordances?srsltid=AfmBOor_Dxj3pjmG-uJP8kqhLjog2_TzmA6cQgTH_ZtACrXKxIndGpze">affordances</a> or the clues that indicate the purpose of an element are very important. Many applications reduce UI elements to their most basic form to save space and reduce clutter, for example, by removing text labels on buttons. This can easily create confusion for all users. My grandfather couldn’t figure out how to send an email because the “send” button was not called out in any way, it didn’t look like a button so it was hard to find on the screen.</p> + +<p>Another issue he ran into was that the UI was so “flat” that there was no distinction between the text input fields for the email subject and body. In a completely flat interface, he didn’t know where he was supposed to type.</p> +<h2 id="reduce-dependency-on-tech-specific-language"> + <a href="#reduce-dependency-on-tech-specific-language"> + Reduce dependency on tech-specific language + </a> +</h2> + +<p>Much of the visual language for UI design is based on the real world. Email is mail, and folders are folders. But imagine trying to explain a hamburger menu icon to a non-tech user. What might be obvious to you is likely not clear to someone else.</p> + +<p>To reduce confusion, we should reduce our dependence on iconography alone to communicate. Text labels go a long way toward adding clarity. The email application my grandfather was using had a side navigation that used icons with no text labels, making it impossible for him to find the emails he had sent. Icons can quickly create confusion if their meaning needs to be clarified.</p> +<h2 id="balance-new-features-with-simplicity"> + <a href="#balance-new-features-with-simplicity"> + Balance new features with simplicity + </a> +</h2> + +<p>Designing software for a broad audience takes work. Power users expect a very different experience compared to first-time users. I believe the clearer the UI, the better the experience will be for all users. We should be careful about adding new features that aren’t essential to the core experience. How are those features explained, and where do they belong on the screen? Progressive disclosure, clear information architecture, and strong hierarchy are important principles when it comes to maintaining clarity in an increasingly complicated UI.</p> + +<p>Everyone is a first-time user at one point, and we should design for that experience without relying on things like product tutorials that are usually seen once. Balance means adding features for power users without inadvertently confusing first-time users.</p> +<h2 id="be-clear"> + <a href="#be-clear"> + Be clear + </a> +</h2> + +<p><a href="https://sensible.com/dont-make-me-think/">Don’t make me think</a>. No, really. The best UI is the UI that no one notices because it’s so simple. Clear and straightforward UI helps users who are not familiar with the software. My grandfather needed to physically write down every step for sending an email so he could repeat the process on his own. He listed everything from how to turn on his “machine” to where to look for responses to his email. We should strive to make our products so simple that new users don’t need to write down how to use them.</p> +<h2 id="personas-can-be-dangerous"> + <a href="#personas-can-be-dangerous"> + Personas can be dangerous + </a> +</h2> + +<p>Personas are great at defining a target audience but can be dangerous if we over-index them. They&rsquo;re meant to form a single identity to “target,” which naturally excludes people who may use our products but don’t happen to fit a certain demographic mold. Our users will have diverse cultural, economic, age, gender, and tech-literacy backgrounds, along with many other factors. These differences may mean the products we design are significantly more complicated to use for those who don’t fit our target persona. We should be careful not to inadvertently exclude folks by relying too heavily on a single persona.</p> + +<p>The next time you are working on a digital product, remember, not everyone using it will be a digital native power user. We should do our best to pause and experience our products through their lens. Put aside the keyboard shortcuts and maybe help a non-tech person send an email to see the digital world through their eyes.</p> + +<hr> + +<p>For additional resources on the topic of ageism in tech, see this thoughtbot article <a href="https://thoughtbot.com/blog/software-for-all-ages-tackling-ageism-with-industry-experts">Software for all ages: tackling ageism with industry experts</a>.</p> + +<p>Check out our past projects with <a href="https://thoughtbot.com/case-studies/airrostiremoterecovery">Airrosti</a>, <a href="https://thoughtbot.com/case-studies/groups-recover-together">Groups Recover Together</a>, and <a href="https://thoughtbot.com/case-studies/relias">Relias</a> to learn more about how we approach the design process with empathy and inclusivity.</p> + +<p>If you are interested in discussing your current design process or need help uncovering the right product strategy and market fit, <a href="https://thoughtbot.com/hire-us">send us an email</a>. We would be happy to help.</p> + + The solution isn’t to just make the text bigger. We need to rethink how we approach product design as a whole. + true + + + Zero-downtime with Rails credentials part II + + + Sami Birnbaum and Valeria Graffeo + + https://thoughtbot.com/blog/from-environment-variables-to-rails-credentials-part-two + 2024-12-18T00:00:00+00:00 + 2024-12-18T15:41:31Z + <p>This post is part of the <a href="https://thoughtbot.com/blog/from-environment-variables-to-rails-credentials">Zero-downtime with Rails credentials</a> series.</p> + +<p>In the <a href="https://thoughtbot.com/blog/from-environment-variables-to-rails-credentials">first post</a> we talked about the reasons that moved us +towards making a codewide change and adopt Rails credentials rather than using +environment variables to manage our secrets.</p> + +<p>In this article we are going to look at the consequences, and the impact that +these have on you as a developer, and your codebase.</p> +<h2 id="what-impact-does-this-change-have-on-you"> + <a href="#what-impact-does-this-change-have-on-you"> + What impact does this change have on you? + </a> +</h2> +<h3 id="credential-files-for-each-environment"> + <a href="#credential-files-for-each-environment"> + Credential files for each environment + </a> +</h3> + +<p>It is important to conceptually consider all the environments +we run the application in. In our case, these are:</p> + +<ul> +<li>development</li> +<li>test</li> +<li>staging</li> +<li>production</li> +</ul> + +<p>Not all of these environments may deploy to a server, indeed both +<code>development</code> and <code>test</code> only exist on your local machine. <code>development</code> +when you run the development server and <code>test</code> when you run your +test suite.</p> + +<p>Currently, when the application runs in any of these environments, +Rails looks for the <code>.env</code> file in order to pick up the environment +variables.</p> + +<p>This is why we currently have a <code>.env</code> file for each of these environments.</p> + +<p>With this change we will be moving away from a <code>.env</code> file to a credential file +for each environment. These will be located at <code>config/credentials/&lt;environment_name&gt;.yml.enc</code>.</p> + +<p>The credential files we expect to have corresponding to each +environment:</p> + +<ul> +<li><code>config/credentials/devlopment.yml.enc</code></li> +<li><code>config/credentials/test.yml.enc</code></li> +<li><code>config/credentials/staging.yml.enc</code></li> +<li><code>config/credentials/production.yml.enc</code></li> +</ul> + +<p>These files are where you will now go to add, edit or remove +credentials, instead of doing so in the <code>.env</code> file.</p> +<h3 id="new-rails-environments"> + <a href="#new-rails-environments"> + New <code>Rails Environments</code> + </a> +</h3> + +<p>Another change that you will start to see is the creation of new +<code>Rails Environments</code> within the Rails app.</p> + +<p>Have a look in <code>config/environments</code>. You will see that as things +currently stand the Rails application is only aware of 3 different +<code>Rails Environments</code>:</p> + +<ul> +<li>development</li> +<li>test</li> +<li>production</li> +</ul> + +<p>When you run the server locally, Rails runs in the development <code>Rails Environment</code>. +When you run the test suite, Rails runs in the test <code>Rails Environment</code>. +You can check this with the command <code>Rails.env</code>.</p> + +<p>However, for all of our other environments, namely <code>staging</code> and <code>production</code>, +Rails treats them all as production <code>Rails Environments</code>. The <code>Rails.env</code> command +returns &ldquo;production&rdquo; in all of these environments.</p> + +<p>This has been set up this way intentionally, using the <code>RAILS_ENV</code> environment +variable in each <code>.env</code> file to set the <code>Rails Environment</code> to production, +like so: <code>RAILS_ENV=production</code>.</p> + +<p>Up until now this didn&rsquo;t really matter. We are happy for Rails +to treat all of the deployable environments as &ldquo;production&rdquo;.</p> + +<p>But with Rails Credentials it does matter&hellip; +Rails uses the <code>Rails Environment</code> to determine +which Rails Credentials file to read from.</p> + +<p>For example when Rails is running in the test <code>Rails Environment</code> +it will know to use the credentials from <code>config/credentials/test.yml.enc</code>.</p> + +<p>But imagine when we have credential files for each of our environments:</p> + +<ul> +<li>staging &ndash;&gt; <code>config/credentials/staging.yml.enc</code></li> +<li>production &ndash;&gt; <code>config/credentials/production.yml.enc</code></li> +</ul> + +<p>The way Rails works is that it will first check what its <code>Rails Environment</code> +(<code>RAILS_ENV</code>) is set to and use that to infer which credential file to +read from.</p> + +<p>As it stands, on all of these environments, the <code>Rails Environemnt</code> +is set to &ldquo;production&rdquo;, in other words <code>RAILS_ENV=production</code>, +and therefore within each environment, Rails will always look for the +<code>config/credentials/production.yml.enc</code> file.</p> + +<p>This is a problem as we want to have different credentials for each +environment, just like we have a different <code>.env</code> files for each +environment.</p> + +<p>In order to do this we have to create a new <code>Rails Environment</code> for each environment, +by creating all the environments in <code>config/environments</code>.</p> + +<p>So we expect to see the following files:</p> + +<ul> +<li><code>config/environments/development.rb</code></li> +<li><code>config/environments/test.rb</code></li> +<li><code>config/environments/staging.rb</code></li> +<li><code>config/environments/production.rb</code></li> +</ul> + +<p>We will also set <code>RAILS_ENV</code> on each environment to the actual +name of the environment and not just production.</p> + +<ul> +<li>staging &ndash;&gt; RAILS_ENV=staging</li> +<li>production &ndash;&gt; RAILS_ENV=production</li> +</ul> + +<p>This will allow Rails to check the <code>RAILS_ENV</code> on the environment +and use that to read from the correct credentials file.</p> + +<p>So, to put it all together:</p> + +<ol> +<li>The Rails app loads on the given environment</li> +<li>Rails checks the value of <code>RAILS_ENV</code> so it knows the <code>Rails Environment</code> it is operating in</li> +<li>Rails uses the correct <code>config/environment</code> file for that environment</li> +<li>Rails uses the correct credentials file for that environment</li> +</ol> +<h3 id="use-a-rails-master-key"> + <a href="#use-a-rails-master-key"> + Use a Rails Master Key + </a> +</h3> + +<p>As mentioned above, all the Rails Credential files will be +encrypted. This is why we can store them safely in version control.</p> + +<p>However, when Rails attempts to read from these credential files, +it will need a key that it can use to decrypt the files and +retrieve the key value pairs in a decrypted state.</p> + +<p>You will need to add the key to your <code>.env</code> file in order +to be able to work with the credential files and decrypt them.</p> + +<p><em>This key is highly sensitive and should not be stored in version +control.</em></p> + +<p>Rails uses a different master key for all the different environments.</p> +<h2 id="so-wait-why-do-we-still-need-a-env-file"> + <a href="#so-wait-why-do-we-still-need-a-env-file"> + So, wait, why do we still need a <code>.env</code> file? + </a> +</h2> + +<p>Well, yes. We will tell you more in our next article, promise.</p> + + Transitioning the codebase from using environment variables to Rails Credentials for Zero-Downtime Deploys. + true + + + Making typography decisions with AI as an assistant + + + Moses Amama + + https://thoughtbot.com/blog/making-typography-decisions-with-ai-as-an-assistant + 2024-12-16T00:00:00+00:00 + 2024-12-16T16:12:21Z + <p>Choosing the right typeface is fundamental in shaping a product’s identity. Designers often rely on their expertise to weigh factors like brand personality, mood, user needs, and cultural nuances—crafting typographic choices that resonate deeply with their users. But with thousands of typefaces available today, where does one even begin? The process can feel overwhelming and time-consuming, with the risk of overlooking great options simply because they’re buried in an endless sea of possibilities.</p> + +<p>This is where AI steps in as a practical ally. It can quickly sift through vast libraries, generate recommendations, and test technical aspects like readability and accessibility. Yet, AI alone cannot replicate the designer’s intuition or the strategic thinking informed by hands-on experience. So, how can we make this process more efficient and impactful? Let’s explore how the typeface selection process unfolds when combining the strengths of AI and a designer’s expertise.</p> +<h2 id="start-with-the-basics"> + <a href="#start-with-the-basics"> + Start with the basics + </a> +</h2> + +<p>Every project begins with a clear understanding of its fundamental purpose. To successfully select an effective typeface for your product, you will need to first identify who will be reading your content, where and how they&rsquo;ll encounter it. You will also need to take into consideration the mood you want your product to have and what typeface can help you amplify that mood. The mood of your typeface should always align with your brand&rsquo;s personality - whether that&rsquo;s projecting professionalism, playfulness, or elegance. During this initial phase, make sure to list out any specific requirements like minimum readable sizes or accessibility standards that need to be met. It will come in handy later when you want to test.</p> + +<p>You can figure out the mood of your product by considering the brand value and purpose, and also by thinking about the audience that will be using the product and the emotional impact you want to have on them.</p> +<h2 id="find-inspiration-with-some-good-ol39-research"> + <a href="#find-inspiration-with-some-good-ol39-research"> + Find inspiration with some good ol&rsquo; research + </a> +</h2> + +<p>It is important to approach any design process with some form of research. Taking the time to see and review what other designers have done with typography on their products, can be really helpful to see what works in existing products and spark some ideas on how to make yours even better. Some of my go-to places to find typography inspiration are Pinterest, Mobbin and good ol&rsquo; google search for products or platforms, I have encountered in the past that evoke certain moods or that I just really liked.</p> +<h2 id="use-ai-tools-to-quickly-generate-initial-ideas"> + <a href="#use-ai-tools-to-quickly-generate-initial-ideas"> + Use AI tools to quickly generate initial ideas + </a> +</h2> + +<p>Using AI as a brainstorming partner has the potential to speed up your design process. I have benefited greatly from incorporating <a href="https://chatgpt.com/">chatGPT</a> and <a href="https://claude.ai/">ClaudeAI</a> into my typeface selection process. These tools help me generate initial typeface suggestions based on the mood, goals and tone that I came up with in the first step. Good prompts are your secret to discovering really great typeface suggesting when brainstorming with AI, Here are some good examples of prompts I have used in the past when doing typeface exploration.</p> + +<ul> +<li>Suggest typefaces for a healthcare website that convey trust, professionalism, and accessibility.</li> +<li>What typefaces would appeal to a younger audience for a fashion and lifestyle brand?</li> +<li>Suggest typefaces that reflect a professional yet approachable personality for a corporate consulting firm.</li> +</ul> + +<p>Once I get some suggestions, I proceed to visualize them in my design tool to help me filter them based on my preferences. Another way to visualize this typeface suggestions more quickly and narrow down your options are through visualization tools like <a href="https://fonts.adobe.com/">AdobeFonts</a>, <a href="https://www.canva.com/">Canva</a> or <a href="https://fontjoy.com/">FontJOY</a>. I have found it is always a good idea to narrow them down to two or three options in this phase.</p> +<h2 id="explore-typeface-pairings-with-some-ai-assistance"> + <a href="#explore-typeface-pairings-with-some-ai-assistance"> + Explore typeface pairings with some AI assistance + </a> +</h2> + +<p>Typeface pairing allows you to combine visual harmony and functionality of two separate typefaces. When selecting typeface combinations, it is often a good idea to look out for typefaces that complement each other while maintaining distinct roles. A common approach is to pair a bold, characterful typeface for headlines with a more neutral, highly readable typeface for body text. The goal is to create a clear visual hierarchy while maintaining harmony across your design. The contrast between your chosen typefaces should be obvious enough to serve a purpose, but not so dramatic that it becomes distracting.</p> + +<p>This an aspect of typeface exploration that AI can save you alot of time on. having narrowed down the choices on the initial typeface to the recommended two or three options, you can rely on AI to generate complementary typefaces that best work with your options based on a defined criteria. An example of prompts I will use to carry out this exercise are:</p> + +<ul> +<li>Recommend a secondary typeface that pairs well with Comfortaa and Garamond, suitable for body text.</li> +<li>What font would complement Comfortaa as a bold headline typeface while maintaining contrast in a thin, readable body font?</li> +<li>Recommend a typeface that creates visual contrast with Garamond without feeling disjointed.</li> +<li>What secondary typeface would pair well with Comfortaa for captions and small text while keeping the design cohesive?</li> +</ul> + +<p>Same as in the step above, It is also a good pairing to filter down your typeface pairing suggestions down to two or three options.</p> +<h2 id="manually-test-and-refine"> + <a href="#manually-test-and-refine"> + Manually test and refine + </a> +</h2> + +<p>Once you have selected your potential typefaces, you would need to test them within the context of the product you are designing. Create actual layouts using your chosen typefaces and examine how they perform across different sizes and formats. Check if headlines are sufficiently impactful and if body text remains comfortable to read in longer passages. Test your typography across various devices and screen sizes to ensure consistency. Pay special attention to readability and accessibility issues; if users are struggling to read your text, then your typography choices have failed their primary purpose.</p> +<h2 id="generate-a-typography-style-system-using-ai"> + <a href="#generate-a-typography-style-system-using-ai"> + Generate a typography style system using AI + </a> +</h2> + +<p>In design, styles form the backbone of consistent design implementation. Beyond just selecting typefaces, you need to establish a comprehensive system of styles that can be applied across your project. This includes creating specific styles for different text elements like headings, subheadings, body text, quotes, and captions. For each style, define not just the typeface family but also precise specifications for size, weight, line height, letter spacing, and color. Consider creating responsive variations that adapt to different screen sizes.</p> + +<p>AI can play a key role in automating the creation of a hierarchy by suggesting font sizes, weights, and spacing that align with typographic best practices and the preference for your project. This shortens the process, ensuring that the typography system feels cohesive, adaptable, and easy to implement across your project. After getting this suggestions, I prefer to take them and organize these styles in a logical hierarchy and name them clearly according to what they will be used for through out the project, this approach helps the team to easily understand and apply them through out the project with little inconsitency. Here is an example of what that will look like:</p> + +<ul> +<li><strong>Main Title</strong> - Comfortaa, 48px/700, #1A1A1A</li> +<li><strong>Section Title</strong> - Comfortaa, 36px/700, #1A1A1A</li> +<li><strong>Card Title</strong> - Comfortaa, 24px/700, #1A1A1A</li> +<li><strong>Body Text</strong> - Inter, 16px/400, #2A2A2A</li> +<li><strong>Button Label</strong> - Inter, 14px/600, #FFFFFF/#1A1A1A</li> +<li><strong>Caption/Helper</strong> - Inter, 12px/400, #6A6A6A</li> +</ul> +<h2 id="manually-create-documentation"> + <a href="#manually-create-documentation"> + Manually create documentation + </a> +</h2> + +<p>When you are done selecting your preferred typefaces, it is important to create clear guidelines for their use. Document not just the chosen typefaces but also specific rules, such as exact sizes for different contexts, spacing requirements, and color combinations. Tools like figma styles and variables allow you to save and apply predefined typography settings across your project, while variables add an extra layer of flexibility. With variables, you can document and organize each style with contextual labels, making it clear when and where each should be used—whether for responsive design, light/dark modes, or different components.</p> + +<p>These tools allows you to ensure consistency across the product while providing an accessible and well-documented system for your team. This approach not only simplifies implementation but also helps maintain the integrity of your design decisions throughout the product.</p> +<h2 id="making-it-work"> + <a href="#making-it-work"> + Making it work + </a> +</h2> + +<p>Making successful typography decisions comes from balancing visual appeal with practical function. Your chosen typefaces should enhance your content&rsquo;s message while reflecting your brand&rsquo;s character, mood and tone. It has to achieve all these while remaining easily readable for the users of the product you are designing. When typography is done right, it becomes an invisible force that guides readers through your content effortlessly. You can begin today to leverage AI to discover newer more amazing typeface options while shortening the time you take to go through your usual typography selection process.</p> + + Combine AI tools with design expertise to make better typography decisions faster. Use AI as an assistant to select, test and implement typefaces while reducing time spent on exploration. + true + + + Testing SQL queries in a Ruby service + + + Sally Hall + + https://thoughtbot.com/blog/testing-sql-queries-in-a-ruby-service + 2024-12-16T00:00:00+00:00 + 2024-12-12T17:17:24Z + <h2 id="the-part-where-our-hero-thinks-everything-is-fine"> + <a href="#the-part-where-our-hero-thinks-everything-is-fine"> + The part where our hero thinks everything is fine + </a> +</h2> + +<p>Recently, I worked on a project where we needed to build a Ruby service that +would run queries on a third party database and send the results to our main +Rails application. The third party database was a huge MSSQL database with tons +of data, but we were only using a handful of tables. As I started building the +service, I wrote specs for the Ruby code, but mocked all the database calls to +return predefined data. This gave me confidence that my Ruby code worked, but +the core purpose of the service was querying the database, and that was entirely +untested.</p> +<h2 id="the-part-where-our-hero-realizes-everything-is-not-in-fact-fine"> + <a href="#the-part-where-our-hero-realizes-everything-is-not-in-fact-fine"> + The part where our hero realizes everything is not, in fact, fine + </a> +</h2> + +<p>I threw a ‘write SQL tests’ card in the tech debt backlog and carried on for a +while, tweaking the queries and ignoring the little voice in the back of my head +that screamed in agony every time I committed a change to a SQL query string +with no corresponding spec. Eventually, it became clear that the queries I had +written were more likely to change over time than I initially thought, and that +everyone else on the project avoided them. It was time to put on my grown up hat +and write some specs.</p> +<h2 id="the-part-where-our-hero-finds-herself-in-unfamiliar-territory"> + <a href="#the-part-where-our-hero-finds-herself-in-unfamiliar-territory"> + The part where our hero finds herself in unfamiliar territory + </a> +</h2> + +<p>This service was missing two things that I am usually able to rely on when +writing specs: Postgres and Rails (specifically, Active Record). It’s fairly +trivial to get Postgres running on my local development environment (I use a +Mac), and on the Docker images I usually use for CI (in this case, +<code>ubuntu-latest</code> on Github Actions).</p> + +<p>One of my first thoughts was to use Postgres for specs and just sort of cross my +fingers that that the syntax I used for queries would work on both database +platforms. In the Ruby service, I used <a href="https://github.com/rails-sqlserver/tiny_tds">TinyTDS</a> to +connect to the external database, which worked really well for actually +executing the queries. I was hoping I could just swap out Postgres for MSSQL and +use TinyTDS with Postgres in the tests, but unfortunately, TinyTDS is for Microsoft SQL or Sybase +databases and there really is no way to make it work with Postgres.</p> +<h2 id="the-part-where-our-hero-contemplates-fleeing-back-to-familiar-grounds"> + <a href="#the-part-where-our-hero-contemplates-fleeing-back-to-familiar-grounds"> + The part where our hero contemplates fleeing back to familiar grounds + </a> +</h2> + +<p>Next, I considered whether refactoring the service to use some Rails components +would make testing easier. I thought I might be able to take advantage of +Active Record to not only make queries simpler, but also make it possible to +connect to a MSSQL database in production and test on a Postgres database, +leaving the hard bits up to the respective adapters. The more I thought about +this, I realized that although it would be a potentially useful refactor in many +ways, attempting a refactor of this complexity on code that is completely +untested was way too risky. Unless I found a time machine that would let me go +back in time and start with Active Record from the first place, I was going to +need to find a way to write specs without it.</p> +<h2 id="the-part-where-our-hero-faces-her-demons-and-begins-to-see-light"> + <a href="#the-part-where-our-hero-faces-her-demons-and-begins-to-see-light"> + The part where our hero faces her demons and begins to see light + </a> +</h2> + +<p>While developing the service initially, I used the Docker image Microsoft +provides to run SQL Server locally. This seemed like a good place to start for +running specs, too. It turned out to be straightforward to remove the mocks from the test code and +have specs use TinyTDS to connect to the test database. I used <a href="https://thoughtbot.com/blog/testing-and-environment-variables">Climate Control</a> +to set the environment variables used by TinyTDS in tests to the values I had used during development.</p> + +<p>Now that I had specs connecting to a database, I needed to set up the database +with the schemas, tables, and functions the service depended on. I also needed a +way to create objects in the database and reset it between runs. In a traditional +Rails application, I usually use <a href="https://github.com/thoughtbot/factory_bot">factory_bot</a> to do this. I took another +moment to appreciate Rails and all the things it brings to my life. I +double checked to see if time travel had become possible, so I could restart the +project with what I had learned. It looked like I was just going to need to +build some tools myself.</p> +<h2 id="the-part-where-our-hero-gains-great-appreciation-for-the-tools-she-can-no-longer-use-and-makes-new-ones"> + <a href="#the-part-where-our-hero-gains-great-appreciation-for-the-tools-she-can-no-longer-use-and-makes-new-ones"> + The part where our hero gains great appreciation for the tools she can no longer use and makes new ones + </a> +</h2> + +<p>First, I created a schema to replicate the portions of the database I needed for +tests. I began by combing through the queries in the service and making note of +every table name and column I used. I quickly realized that my existing specs +could help with this! When I ran a spec that used a table or column that was +missing, I would get an error message with the missing database object. I kept +adding tables to my local test database until none of the spec failure messages +were about a missing database object. I now had the minimum schema necessary to +test the service.</p> + +<p>Our service relied on a custom function in the database. I needed to be able to +call this function for the tests to work properly, but it wasn’t necessary to +completely replicate the function and its logic. Instead, I created a table into +which I could insert the inputs and outputs of the function. Then I created a +function that would look up a row given the input and return the output. This +allowed me to mock the database function call without changing the database +query.</p> + +<p>As I built the database through this process, I kept track of the SQL commands I +used to create tables. These commands together became a schema that could be +loaded into the database when setting it up for tests. I created a <code>DataHelper</code> +module to use in specs that would use these commands to set up a test database +or clean the test database between examples.</p> +<div class="highlight"><pre class="highlight ruby"><code><span class="no">RSpec</span><span class="p">.</span><span class="nf">configure</span> <span class="k">do</span> <span class="o">|</span><span class="n">config</span><span class="o">|</span> + <span class="n">config</span><span class="p">.</span><span class="nf">before</span><span class="p">(</span><span class="ss">:suite</span><span class="p">)</span> <span class="k">do</span> + <span class="no">DataHelpers</span><span class="p">.</span><span class="nf">setup_database</span> + <span class="k">end</span> + <span class="n">config</span><span class="p">.</span><span class="nf">before</span><span class="p">(</span><span class="ss">:each</span><span class="p">)</span> <span class="k">do</span> + <span class="no">DataHelpers</span><span class="p">.</span><span class="nf">clean_database</span> + <span class="k">end</span> +<span class="k">end</span> +</code></pre></div><div class="highlight"><pre class="highlight ruby"><code><span class="k">module</span> <span class="nn">DataHelpers</span> + <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">setup_database</span> + <span class="n">drop_tables</span> + <span class="n">drop_address_match_function</span> + <span class="n">create_schemas</span> + <span class="n">load_schema</span> + <span class="n">create_address_match_function</span> + <span class="k">end</span> + +<span class="o">...</span> + +<span class="k">end</span> +</code></pre></div> +<p>Now I had a test suite that connected to a local MSSQL database and could set up +and clean the database when running tests. All I had left was to create data +during tests. Since I didn’t have objects defined in the service to encapsulate +the data yet (have I mentioned how much I wish I had used Active Record?), I +couldn’t rely on factory_bot to create data. Instead, I created methods to run +insert queries with reasonable defaults and called them the way I would use +factory bot. Unlike factory_bot, these methods do not return the object that was +created, but they worked well enough without that.</p> +<div class="highlight"><pre class="highlight ruby"><code><span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">create_user</span><span class="p">(</span><span class="n">email</span><span class="p">,</span> <span class="n">full_name</span><span class="p">,</span> <span class="n">admin</span> <span class="o">=</span> <span class="kp">false</span><span class="p">)</span> + <span class="n">client</span><span class="p">.</span><span class="nf">execute</span><span class="p">(</span><span class="s2">"INSERT INTO User( + Email, + FullName, + Admin) + VALUES ( </span><span class="si">#{</span><span class="n">email</span><span class="si">}</span><span class="s2">, </span><span class="si">#{</span><span class="n">full_name</span><span class="si">}</span><span class="s2">, </span><span class="si">#{</span><span class="n">admin</span><span class="si">}</span><span class="s2">)"</span><span class="p">).</span><span class="nf">do</span> +<span class="k">end</span> +</code></pre></div> +<p>At this point, I had a test suite that connected to a test MSSQL database, +actually tested the database queries, and set up/cleaned up the database so it +worked for multiple test runs.</p> + +<aside class="info"> + <p>I also had to run tests on CI which <a href="https://gist.github.com/sallyhall/26bd056cb2efc42847828840ce6b072d">required some setup</a>.</p> +</aside> +<h2 id="the-part-where-our-hero-reflects-on-the-lesson-she-learned-the-hard-way"> + <a href="#the-part-where-our-hero-reflects-on-the-lesson-she-learned-the-hard-way"> + The part where our hero reflects on the lesson she learned the hard way + </a> +</h2> + +<p>When I began writing this Ruby service, I assumed I would simply be connecting to +an external database, running a few queries, and forwarding on the results to our +main Rails application. Surely something this straightforward doesn&rsquo;t need all +of Rails! I can just make some little helper classes and move on. It turns out, +the service itself really does &ldquo;simply&rdquo; connect to the database, make queries, +and send data to the app, but simple rarely means easy.</p> + +<p>When I chose my tools, I was only considering writing and running the code. I +didn&rsquo;t take any time to think through the best way to test it. If I had begun my +design and planning with a testing strategy, I may have discovered the pitfalls I +encountered before I started wishing for a time machine. Test-driven development +doesn&rsquo;t start when we write the first line of code. Next time, our hero will +consider testing as part of every decision.</p> + + An epic journey of facing MSSQL and missing Rails. + true + + diff --git a/spec/readme_spec.rb b/spec/readme_spec.rb new file mode 100644 index 0000000..2816068 --- /dev/null +++ b/spec/readme_spec.rb @@ -0,0 +1,47 @@ +require "tmpdir" + +require "spec_helper" +require "github_readme" + +RSpec.describe Readme do + describe "#update" do + it "sets the content" do + with_temp_directory do |dir| + File.write("README.md", readme_body) + readme = described_class.new("README.md") + items = [ + FeedItem.new( + id: nil, + title: "Optimising your shell experience", + url: "https://thoughtbot.com/blog/optimize-your-shell-experience", + created_at: Date.new(2024, 12, 20), + updated_at: nil + ) + ] + + readme.update(section: "blog", items: items) + + expect(File.read("README.md")).to include( + "[Optimising your shell experience](https://thoughtbot.com/blog/optimize-your-shell-experience)" + ) + end + end + end + + def with_temp_directory + Dir.mktmpdir do |dir| + Dir.chdir dir do + yield(dir) + end + end + end + + def readme_body + <<~BODY + Some intro text + + + + BODY + end +end diff --git a/spec/rss_feed_spec.rb b/spec/rss_feed_spec.rb new file mode 100644 index 0000000..b9ef943 --- /dev/null +++ b/spec/rss_feed_spec.rb @@ -0,0 +1,36 @@ +require "spec_helper" +require "github_readme" + +RSpec.describe RssFeed do + describe "#recent" do + it "fetches the top 5 most recent posts" do + Excon.stub({}, { status: 200, body: atom_feed }) + rss_feed = described_class.new( + url: "https://feeds.feedburner.com/GiantRobotsSmashingIntoOtherGiantRobots" + ) + + recent = rss_feed.take(5) + + expect(recent.count).to eq(5) + end + + it "builds a FeedItem from a feed item" do + Excon.stub({}, { status: 200, body: atom_feed }) + rss_feed = described_class.new( + url: "https://feeds.feedburner.com/GiantRobotsSmashingIntoOtherGiantRobots" + ) + + recent = rss_feed.take(5) + + expect(recent.first).to have_attributes( + title: "Optimize your shell experience", + url: "https://thoughtbot.com/blog/optimize-your-shell-experience", + created_at: Time.new(2024, 12, 20) + ) + end + + def atom_feed + File.read("spec/fixtures/rss_feed.xml") + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..6388b5f --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,11 @@ +require "rspec" + +RSpec.configure do |config| + config.before(:all) do + Excon.defaults[:mock] = true + end + + config.after(:each) do + Excon.stubs.clear + end +end