<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>kevinluo201 (Kevin Luo)</title>
    <link>https://ruby-china.org/kevinluo201</link>
    <description>Kevinluo201</description>
    <language>en-us</language>
    <item>
      <title>亲爱的 AI，把 Rails Guide 翻成中文吧~</title>
      <description>&lt;p&gt;最近做了个实验用，ChatGPT api 把最新的 Rails Guide 翻译成中文，好像还行啊~ 如果有什麽想法欢迎一起讨论一下 😁&lt;/p&gt;

&lt;p&gt;实验成果：&lt;a href="https://ai.rails-guide.com/zh-CN/" rel="nofollow" target="_blank"&gt;https://ai.rails-guide.com/zh-CN/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Blog: &lt;a href="https://dev.to/kevinluo201/dear-ai-can-you-translate-the-rails-guide-for-me-3hc" rel="nofollow" target="_blank"&gt;https://dev.to/kevinluo201/dear-ai-can-you-translate-the-rails-guide-for-me-3hc&lt;/a&gt;&lt;/p&gt;</description>
      <author>kevinluo201</author>
      <pubDate>Mon, 31 Jul 2023 04:32:05 +0800</pubDate>
      <link>https://ruby-china.org/topics/43244</link>
      <guid>https://ruby-china.org/topics/43244</guid>
    </item>
    <item>
      <title>做了一个算节气的 gem</title>
      <description>&lt;p&gt;&lt;a href="https://github.com/kevinluo201/solar_terms_24" rel="nofollow" target="_blank" title=""&gt;https://github.com/kevinluo201/solar_terms_24&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;前阵子研究农曆时，才发现原来农曆是阴阳合曆，用「阴曆」计算月份，用「节气」来置闰。又发现节气如「清明」、「冬至」等虽然每年在阳曆上都是固定某几天，但总有个 3 天的区间，比如明年清明节究竟是 4 月 4 日、5 日还是 6 日，只能看月曆了。仔细再查原来节气竟是用地球在黄道上与太阳的角度决定的！&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/kevinluo201/5ed991dd-be68-457d-b53c-b7b929fdb7ba.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;要知道节气的日期，就必须要有那些资料。要取得这些资料，只能靠夜观星象纪录下来，并统计推算了。幸好，NASA 的 JPL 实验室有推出 Horizons 系统有 API 可以算出太阳系内所有行星的数据。利用该 API 可以取得计算节气所需的角度和时间，进一步来算出节气。&lt;/p&gt;

&lt;p&gt;另一个问题是其实节气是一个固定的时段，在不同国家如韩国、日本、越南，因时区其实各地的节气日期会不同。 &lt;code&gt;solar_terms_24&lt;/code&gt; 可以做时区及语言的转换。&lt;/p&gt;

&lt;p&gt;基本上我已将 1900-2100 年的节气日期都先存起来了，不过如果去求超出范围的年份，可以即时运算。提供给有需要的人了。&lt;/p&gt;</description>
      <author>kevinluo201</author>
      <pubDate>Sat, 31 Dec 2022 00:17:59 +0800</pubDate>
      <link>https://ruby-china.org/topics/42815</link>
      <guid>https://ruby-china.org/topics/42815</guid>
    </item>
    <item>
      <title>介绍 RSpec Request Spec</title>
      <description>&lt;p&gt;&lt;a href="https://dev.to/kevinluo201/rspec-request-spec-4781" rel="nofollow" target="_blank"&gt;https://dev.to/kevinluo201/rspec-request-spec-4781&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;这次想介绍 RSpec 的 Request spec&lt;/p&gt;
&lt;h2 id="什麽 Request spec? 为何推荐使用它？"&gt;什麽 Request spec? 为何推荐使用它？&lt;/h2&gt;
&lt;p&gt;Request spec 故名思义，是专门测 &lt;strong&gt;HTTP 请求&lt;/strong&gt;的测试。一个 web 的应用，其实也可以说是一个用 http request 跟伺服器做互动的程式。
一个合格的 Rails 开发者，我们通常 model 的测试复盖率还不错 (是吧？)。不过那只能保证比较不会有写入错误资料进资料库的事情发生。使用者基本上不会直接去呼叫你的 model 的方法，他们会直接打 request 到你的伺服器。结果我们却不测这件事好像不是很合理？&lt;/p&gt;

&lt;p&gt;好啦，其实有写 model test 的团队就谢天谢地了，很常看到开发的团队会直接跳过 request test 直接做 end-to-end 的人工测试，或者直接让 QA 来做 end-to-end 的测试。
如果是一个短期的专案，老实说这也不是什麽大问题。
但如果是一个为长期的专案，通常就是指你现在在公司上班都要维护的专案，恐怕常常是程式出错时，但错的地方根本不是程式码修改的地方。其实是因为就算我们的 model test 的复盖率高到 100%，也不能保证这件元件互动也是完全没问题。而有 Request test 就有较高的机会前提发现这些错误。&lt;/p&gt;

&lt;p&gt;下图是一个机器运作正常但还是出现意料之外结果的范例：&lt;br&gt;
&lt;img src="https://l.ruby-china.com/photo/kevinluo201/1b781f85-dfbc-41d2-b023-30a2c599a956.png!large" title="" alt=""&gt;
这是 gif，好像上载失败了...就当它是倒垃圾失败的吧&lt;/p&gt;

&lt;p&gt;我觉得写 Request test 可以帮开发者花少一点时间 debug。&lt;/p&gt;
&lt;h2 id="要用 RSpec 的Request spec 或 Controller spec?"&gt;要用 RSpec 的 Request spec 或 Controller spec?&lt;/h2&gt;
&lt;p&gt;RSpec 已经有一个 Controller Spec，就是专门来测 controller 的。那为什麽不用 controller spec 来测 controller 而是用 request?
第一个理由是因为 Request spec 会运行一个 HTTP request 会用到的所有层面，例：routing, views 甚至 rack middleware。而 controller spec 只有单独测 controller action，除非自己还分别再去写 routing spec, view spec 那 request spec 写起来似乎 CP 值较高。&lt;/p&gt;

&lt;p&gt;另一个理由就比较单纯了，RSpec 开发团队推荐直接用 Request spec:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For new Rails apps: we don't recommend adding the rails-controller-testing gem to your application. The official recommendation of the Rails team and the RSpec core team is to write request specs instead. Request specs allow you to focus on a single controller action, but unlike controller tests involve the router, the middleware stack, and both rack requests and responses. This adds realism to the test that you are writing, and helps avoid many of the issues that are common in controller specs. In Rails 5, request specs are significantly faster than either request or controller specs were in rails 4, thanks to the work by Eileen Uchitelle of the Rails Committer Team.  &lt;/p&gt;
&lt;h2 id="Request spec 特点"&gt;Request spec 特点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;会执行一个 request 时全栈的程式，包括：会执行 routing，会跑 controller action，会渲染 erb 等。&lt;/li&gt;
&lt;li&gt;速度快 (跟 capybara 比的话)&lt;/li&gt;
&lt;li&gt;可以在一个测试范例中做数个 request，甚至可以跟随 redirect 到下一页
```ruby
it "creates a Widget and redirects to the Widget's page" do
get "/widgets/new"
expect(response).to render_template(:new)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;post "/widgets", :params =&amp;gt; { :widget =&amp;gt; {:name =&amp;gt; "My Widget"} }
  expect(response).to redirect_to(assigns(:widget))
  follow_redirect!
  expect(response).to render_template(:show)
  expect(response.body).to include("Widget was successfully created.")
end&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
## 什麽时候不适合用 Request spec 
* 因为 Request spec 不会执行任何 Javascript，所以如果你的画面是用 vue, react 渲染，又想确定指定的元素是否有被渲染出来时，就不适合用
* 同上，如果目标是想看画面上的使用者互动，因为那些互动也都是 Javascript，所以也不行。(但开发者应该是要为 js 打的 API写  request test)

Capybara 那种自动的 end-to-end 测试不在这次的讨论范围内~
不过如果是一个一般的 Rails 专案，我想 request spec 还是可以 cover 大部分的情况啦

## 使用方式
我就不介绍 RSpec 引入 Rails 的方式了，直接介绍 request spec

### 安装
如果想要在 Request spec 用 rails  routing 的 helpr 的话，像 root_url 这种，可以在 spec_helper 引入
```ruby
RSpec.configure do |config|
  config.include Rails.application.routes.url_helpers, type: :request
  # ...
end
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;其实只要将在 &lt;code&gt;spec/&lt;/code&gt; 下的测试档 &lt;code&gt;*_spec.rb&lt;/code&gt; 的 RSpec.describe 后加上 &lt;code&gt;type: :request&lt;/code&gt; ，rspec 即知道要做 request spec 了
&lt;code&gt;ruby
RSpec.describe "/some/path", type: :request do
# spec 的内容
end
&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;或是把 &lt;code&gt;*_spec.rb&lt;/code&gt; 的测试档放到 &lt;code&gt;spec/requests/&lt;/code&gt;下，RSpec 也会直接假设那些档案都是要做 request spec
## 如何做 request?
Request spec 既然要测 HTTP 请求 (request)，当然也有提供对应的方法了：&lt;/li&gt;
&lt;li&gt;get&lt;/li&gt;
&lt;li&gt;post&lt;/li&gt;
&lt;li&gt;patch&lt;/li&gt;
&lt;li&gt;put&lt;/li&gt;
&lt;li&gt;delete
这些方式都是长这样： &lt;code&gt;post(url, options = {})&lt;/code&gt;
可以放 &lt;code&gt;params&lt;/code&gt; 和 &lt;code&gt;headers&lt;/code&gt; 在 options 裡。
&lt;code&gt;ruby
# 这些方法后面的 url 可写完整路径，也可以用 route 的方法
get root_url
get "/articles?page=3"
post users_url, params: "{\"name\": \"Kevin\"}", headers: {"Content-Type" =&amp;gt; "application/json"}
patch "/users/2", params: "{\"height\": 183}", headers: {"Content-Type" =&amp;gt; "application/json"}
delete user_url(User.find(2)), headers: {"Authorization" =&amp;gt; "Bearer #{@token}"}
&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="常见问题，如何传档案？"&gt;常见问题，如何传档案？&lt;/h3&gt;
&lt;p&gt;可以利用 &lt;code&gt;Rack::Test::UploadedFile&lt;/code&gt;, 例如&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;let&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:filepath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'spec'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'fixtures'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'blank.jpg'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;let&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="no"&gt;Rack&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Test&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;UploadedFile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'image/jpg'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;# 在测试中可这样用&lt;/span&gt;
&lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="n"&gt;upload_image_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;params: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;file: &lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="如何做断言(assertions)?"&gt;如何做断言 (assertions)?&lt;/h2&gt;
&lt;p&gt;断言是测试中最重要的事，在 rspec 裡就是那些 expect
我们可以用 expect 去验证 response 及 controller 内执行完的结果
测 request test 比较接近"黑盒测试"，也就是尽量只要管出输入输出殆可。&lt;/p&gt;

&lt;p&gt;我们可以用 &lt;code&gt;@response&lt;/code&gt; 或 &lt;code&gt;response&lt;/code&gt;来取得回应的物件&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 我们可以验证 response 的 http 状态&lt;/span&gt;
&lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;have_http_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# 200&lt;/span&gt;
&lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;have_http_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:accepted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# 202&lt;/span&gt;
&lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;have_http_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:not_found&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# 404&lt;/span&gt;

&lt;span class="c1"&gt;# 我们可以验证 redirect 的网址&lt;/span&gt;
&lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;redirect_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;articles_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 我们可以验证是渲染了哪个 template 或 partial&lt;/span&gt;
&lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;render_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;render_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"articles/_article"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 可以验证 response body 的容易字串&lt;/span&gt;
&lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="kp"&gt;include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;h1&amp;gt;Hello World&amp;lt;/h1&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 也可以直接看说有没有对资料库存取&lt;/span&gt;
&lt;span class="n"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="n"&gt;articles_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;params: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;title: &lt;/span&gt;&lt;span class="s1"&gt;'A new article'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;change&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="no"&gt;Article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="有办法做更细的 DOM 断言吗？"&gt;有办法做更细的 DOM 断言吗？&lt;/h3&gt;
&lt;p&gt;是可以的，我们可以利用 &lt;code&gt;ActionController::Assertions::SelectorAssertions&lt;/code&gt;&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# assert_select 可以直接用 css 选择器去选 id="some_element" 的元素&lt;/span&gt;
&lt;span class="n"&gt;assert_select&lt;/span&gt; &lt;span class="s2"&gt;"#some_element"&lt;/span&gt; 

&lt;span class="c1"&gt;# 直接验证所有的 ol 都要有 4 个 li&lt;/span&gt;
&lt;span class="n"&gt;assert_select&lt;/span&gt; &lt;span class="s2"&gt;"ol"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;elements&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;elements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;assert_select&lt;/span&gt; &lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"li"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;assert_select&lt;/span&gt; &lt;span class="s2"&gt;"ol"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;assert_select&lt;/span&gt; &lt;span class="s2"&gt;"li"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 APIdock 上有更详细的文件 &lt;a href="https://apidock.com/rails/ActionController/Assertions/SelectorAssertions/assert_select" rel="nofollow" target="_blank" title=""&gt;assert_select (ActionController::Assertions::SelectorAssertions) - APIdock&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="在 spec example可存取的变数"&gt;在 spec example 可存取的变数&lt;/h2&gt;
&lt;p&gt;除了刚刚提到的 &lt;code&gt;@reponse&lt;/code&gt; ，下面变数也可以存取&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;assigns: instance variable 像 &lt;a href="/user" class="user-mention" title="@user"&gt;&lt;i&gt;@&lt;/i&gt;user&lt;/a&gt; 可以从 assigns 来存取，例&lt;code&gt;assigns[:user]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;sessions&lt;/li&gt;
&lt;li&gt;flash&lt;/li&gt;
&lt;li&gt;cookies&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="如何跟 Devise 整合？"&gt;如何跟 Devise 整合？&lt;/h2&gt;
&lt;p&gt;We can use Devise helper if we use Devise to do the authentication.
如果有用 Devise 做登入的话，可以使用 Devise 的 &lt;code&gt;Devise::Test::IntegrationHelpers&lt;/code&gt;&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# spec_helper.rb&lt;/span&gt;
&lt;span class="no"&gt;RSpec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Devise&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Test&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;IntegrationHelpers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: :request&lt;/span&gt; &lt;span class="c1"&gt;# to sign_in user by Devise&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# 在范例中可以用 sign_in 登入&lt;/span&gt;
&lt;span class="n"&gt;let&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s2"&gt;"an example"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;sign_in&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;
  &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s2"&gt;"/articles"&lt;/span&gt;
  &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;have_http_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;render_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为许多页面都需要使用者登入，所我通常会做一个 shared_context，然后在需要的地方引入&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;RSpec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shared_context&lt;/span&gt; &lt;span class="ss"&gt;:login_user&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;let&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;sign_in&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# then use include_context to include it&lt;/span&gt;
&lt;span class="n"&gt;include_context&lt;/span&gt; &lt;span class="ss"&gt;:login_user&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="个人经验"&gt;个人经验&lt;/h2&gt;
&lt;p&gt;其实老实说我以前也很少写 request spec。
因为我觉得 model 的方法是真的逻辑在的地方，controller 只是把 model 方法的结果带到 erb 去渲染而已。我们干嘛去验证这一层？这应该是 Rails 本身的功能，是它负责的。&lt;/p&gt;

&lt;p&gt;但我的想法太理想了..., 现实的状况花样百出，许多画面或 API 都是混合一堆 model 的方法或多个 service object 的结果，erb 裡也常有很複杂的方法
Ruby 也不是强型别的语言，所以也看不出来什麽问题，最后上线后就直接给你一个 500
&lt;img src="https://l.ruby-china.com/photo/kevinluo201/2ae7fffb-67c7-43d4-9269-ab1417764636.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;当这个红画面出现时，即使工程师说：「ok 啦...小问题，没有错误资料写入资料库，一下就修復！」我不觉得相关人士尤其是公司老闆听到会有多开/放心...&lt;/p&gt;

&lt;p&gt;我觉得完整的 request spec 可以减少这类问题发生的机率。
尤实是 Request spec 包含了几乎可以说是全栈的互动在内。如果 Request spec 有过，那几乎等于真的在浏览器上也可以过了。&lt;/p&gt;

&lt;p&gt;我发现用 scaffold 产生的 request spec 的结构非常好，十分推荐大家直接用它的架构，我贴在这：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;RSpec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt; &lt;span class="s2"&gt;"/articles"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: :request&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;let&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:valid_attributes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;skip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"加 controller strong params 允许的全部参数"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;let&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:invalid_attributes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;skip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"加非法的参数"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="s2"&gt;"GET /index"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s2"&gt;"renders a successful response"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="no"&gt;Article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt; &lt;span class="n"&gt;valid_attributes&lt;/span&gt;
      &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="n"&gt;articles_url&lt;/span&gt;
      &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;be_successful&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="s2"&gt;"GET /show"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s2"&gt;"renders a successful response"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;article&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt; &lt;span class="n"&gt;valid_attributes&lt;/span&gt;
      &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="n"&gt;article_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;be_successful&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="s2"&gt;"GET /new"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s2"&gt;"renders a successful response"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="n"&gt;new_article_url&lt;/span&gt;
      &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;be_successful&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="s2"&gt;"GET /edit"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s2"&gt;"render a successful response"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;article&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt; &lt;span class="n"&gt;valid_attributes&lt;/span&gt;
      &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="n"&gt;edit_article_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;be_successful&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="s2"&gt;"POST /create"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="s2"&gt;"with valid parameters"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s2"&gt;"creates a new Article"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="n"&gt;articles_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;params: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;article: &lt;/span&gt;&lt;span class="n"&gt;valid_attributes&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;change&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Article&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:count&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s2"&gt;"redirects to the created article"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="n"&gt;articles_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;params: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;article: &lt;/span&gt;&lt;span class="n"&gt;valid_attributes&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;redirect_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;last&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="s2"&gt;"with invalid parameters"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s2"&gt;"does not create a new Article"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="n"&gt;articles_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;params: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;article: &lt;/span&gt;&lt;span class="n"&gt;invalid_attributes&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;change&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Article&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:count&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s2"&gt;"renders a successful response (i.e. to display the 'new' template)"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="n"&gt;articles_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;params: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;article: &lt;/span&gt;&lt;span class="n"&gt;invalid_attributes&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;be_successful&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="s2"&gt;"PATCH /update"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="s2"&gt;"with valid parameters"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;let&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:new_attributes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;skip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Add a hash of attributes valid for your model"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s2"&gt;"updates the requested article"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;article&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt; &lt;span class="n"&gt;valid_attributes&lt;/span&gt;
        &lt;span class="n"&gt;patch&lt;/span&gt; &lt;span class="n"&gt;article_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;params: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;article: &lt;/span&gt;&lt;span class="n"&gt;new_attributes&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reload&lt;/span&gt;
        &lt;span class="n"&gt;skip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Add assertions for updated state"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s2"&gt;"redirects to the article"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;article&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt; &lt;span class="n"&gt;valid_attributes&lt;/span&gt;
        &lt;span class="n"&gt;patch&lt;/span&gt; &lt;span class="n"&gt;article_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;params: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;article: &lt;/span&gt;&lt;span class="n"&gt;new_attributes&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reload&lt;/span&gt;
        &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;redirect_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="s2"&gt;"with invalid parameters"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s2"&gt;"renders a successful response (i.e. to display the 'edit' template)"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;article&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt; &lt;span class="n"&gt;valid_attributes&lt;/span&gt;
        &lt;span class="n"&gt;patch&lt;/span&gt; &lt;span class="n"&gt;article_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;params: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;article: &lt;/span&gt;&lt;span class="n"&gt;invalid_attributes&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;be_successful&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="s2"&gt;"DELETE /destroy"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s2"&gt;"destroys the requested article"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;article&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt; &lt;span class="n"&gt;valid_attributes&lt;/span&gt;
      &lt;span class="n"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;delete&lt;/span&gt; &lt;span class="n"&gt;article_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;change&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Article&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:count&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s2"&gt;"redirects to the articles list"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;article&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt; &lt;span class="n"&gt;valid_attributes&lt;/span&gt;
      &lt;span class="n"&gt;delete&lt;/span&gt; &lt;span class="n"&gt;article_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;redirect_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;articles_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实会发现没有那麽难写，如果想要它变得短一些，也可以用 &lt;code&gt;FactoryBot&lt;/code&gt; 之类的工具
另外，如果是 create 或 update 成功的测试，我会再进一步去验证纪录的内容：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;RSpec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt; &lt;span class="s2"&gt;"/articles"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: :request&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;let&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:valid_attributes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="ss"&gt;title: &lt;/span&gt;&lt;span class="s1"&gt;'文章标题'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;contenxt: &lt;/span&gt;&lt;span class="s1"&gt;'这是一篇文章'&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="s2"&gt;"POST /create"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="s2"&gt;"with valid parameters"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s2"&gt;"creates a new Article"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="n"&gt;articles_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;params: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;article: &lt;/span&gt;&lt;span class="n"&gt;valid_attributes&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;change&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Article&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:count&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;article&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;last&lt;/span&gt;
        &lt;span class="c1"&gt;# 不论有多少个 attributes，每一个我都会分别验证 &lt;/span&gt;
        &lt;span class="c1"&gt;# 而不会再用任何 "聪明的" 方式去减少要写的程式码了&lt;/span&gt;
        &lt;span class="c1"&gt;# 我不想造成伪阴性&lt;/span&gt;
        &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'文章标题'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'这是一篇文章'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="结论"&gt;结论&lt;/h2&gt;
&lt;p&gt;我现在心中的测试金字塔约略长这样：
&lt;img src="https://l.ruby-china.com/photo/kevinluo201/35f05af0-f266-4d63-af71-675990ca52dd.png!large" title="" alt=""&gt;
(system tests 是指 end-to-end test，Rails 这样命名...)
The width of each level mean the number of the tests should exist in the system.
金字塔的宽度是测试的数量。
虽然 Model tests 是最多，但它们通常就是测一个 model 的一个方法，是一个很小的范围，在 rails 裡可算是 unit test。&lt;/p&gt;

&lt;p&gt;很多团队根本就完全手动执行 system test，如果运气好的话，会有专门的 QA 团队协助。但随时系统功能越来越多，QA 团队会有一个超级庞大的测试清单。为了完成全部的测试项目，交付程式码的时程会越来越长。QA 也会过劳而系统又持续一堆 bug. &lt;/p&gt;

&lt;p&gt;如果我们加了 model test 再上一层的 request test，我们可以减少手动测试的数量，除了系统更稳定外也避免 QA 集体离职...。QA 可以做更关键的测试，比如信用卡付款之类的；而不是为了怕有一些页面可能会坏掉，所以每次部署前都要点开所有页面一遍这种没什麽特别意义的测试。&lt;/p&gt;
&lt;h2 id="参考资料:"&gt;参考资料：&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt; &lt;a href="https://relishapp.com/rspec/rspec-rails/v/5-0/docs/request-specs/request-spec" rel="nofollow" target="_blank" title=""&gt;https://relishapp.com/rspec/rspec-rails/v/5-0/docs/request-specs/request-spec&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;&lt;a href="http://rspec.info/blog/2016/07/rspec-3-5-has-been-released/" rel="nofollow" target="_blank" title=""&gt;RSpec 3.5 has been released!&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>kevinluo201</author>
      <pubDate>Sun, 05 Sep 2021 23:21:32 +0800</pubDate>
      <link>https://ruby-china.org/topics/41656</link>
      <guid>https://ruby-china.org/topics/41656</guid>
    </item>
    <item>
      <title>利用 systemd 在 Ubuntu 執行 Sidekiq 6</title>
      <description>&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/kevinluo201/b9c06389-175f-455a-8f03-4e47845bb9b5.jpg!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;网志版：&lt;a href="https://dev.to/kevinluo201/systemd-ubuntu-sidekiq-6-d9c" rel="nofollow" target="_blank" title=""&gt;https://dev.to/kevinluo201/systemd-ubuntu-sidekiq-6-d9c&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;自从有了 Sidekiq 后，所有事情都变得容易了。
所有需要长时间执行的程式，我全往 sidekiq 扔，我再也不用怕 CloudFlare 的 524(请求超时) 了 (咦？)
在 production 的环境执行 sidekiq 虽不困难但对于 Linux 没那麽熟的人来说还满多细节要注意的，
Sidekiq 6 又好像跟之前变化颇大。
这次想分享一下将 Sidekiq 6 运行在 ubuntu 20.04 的经验 : )&lt;/p&gt;
&lt;h2 id="目标"&gt;目标&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;运行 Sidekiq (废话...)&lt;/li&gt;
&lt;li&gt;重开机或 sidekiq 挂了的话，sidekiq 会自动重启&lt;/li&gt;
&lt;li&gt;当我用 Capistrano 部署新的程式码时，sidekiq 会读新的程式码并重启&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="我们怎麽执行 Sidekiq?"&gt;我们怎麽执行 Sidekiq?&lt;/h2&gt;
&lt;p&gt;要知道在 production 环境怎麽运行 sidekiq, 首先要知道在 development 环境是怎麽跑的：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle &lt;span class="nb"&gt;exec &lt;/span&gt;sidekiq
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实就那麽容易... 你可以再加一些参数像 &lt;code&gt;-C xxxx.yml&lt;/code&gt; 去指定要读取的设定档。不过我们先维持这样&lt;/p&gt;

&lt;p&gt;基本上你在 production 环境可以做一样的事，如果你想让它更像一个”真的服务”，也可以直接加个“&amp;amp;”在后面让它跑在背景：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;RAILS_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;production bundle &lt;span class="nb"&gt;exec &lt;/span&gt;sidekiq &amp;amp;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果真的纯粹只是想要在 production 环境跑 sidekiq，其实这样就够了&lt;/p&gt;

&lt;p&gt;但光这样做好像没符合我们的目标：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;重开机或程式挂掉后 sidekiq 不会自动运行&lt;/li&gt;
&lt;li&gt;部署完后也不会自动运行&lt;/li&gt;
&lt;li&gt;最重要的是，这感觉好像不是很”Pro”。即使我们不是用 Sidekiq Pro , 应该也可以 Pro 一点&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;好，为了达到我们的目标，我们需要用 &lt;code&gt;systemd&lt;/code&gt;&lt;/p&gt;
&lt;h2 id="systemd是什麽?"&gt;systemd 是什麽？&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;systemd&lt;/code&gt; 是 Linux 系统专门来管理各式「服务程式」的程式，其实就是 daemon 所以才是 system*&lt;em&gt;d&lt;/em&gt;*。比如 mysql, apache, nginx, redis 这些都可以用它来管，事实上 &lt;code&gt;systemd&lt;/code&gt; 是多数 Linux 版本预设的 Service Manager。
有 &lt;code&gt;systemd&lt;/code&gt;,，我们可以：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;用 &lt;code&gt;systemctl start/stop/restart&lt;/code&gt; 任何服务&lt;/li&gt;
&lt;li&gt;可以启用 (&lt;strong&gt;enable&lt;/strong&gt;) 服务，启用的服务在系统重开时会自动开始运行&lt;/li&gt;
&lt;li&gt;你可以指定当程式挂掉后，该做什麽事，例如重启该服务&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;systemd&lt;/code&gt; 的内容还有一堆，不过目前知道这样就足够了，剩下的就自行 google 吧 xD
看起来 systemd 可以符合我们想做的事，就用 systemd 来操作 sidekiq 吧！&lt;/p&gt;
&lt;h2 id="将 Sidekiq 变成一个服务单元(service unit)"&gt;将 Sidekiq 变成一个服务单元 (service unit)&lt;/h2&gt;
&lt;p&gt;在 systemd 中，每个服务都被视为一个「单元」(Unit)
要新增一个 sidekiq 的服务单元，我们可以新增一个档案 &lt;code&gt;/lib/systemd/system/sidekiq.service&lt;/code&gt;
(另外，用来部署 rails 的使用者叫 &lt;code&gt;deployer&lt;/code&gt;)&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# /lib/systemd/system/sidekiq.service&lt;/span&gt;
&lt;span class="c"&gt;# 我们的 service 叫 sidekiq&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;Unit]
&lt;span class="nv"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sidekiq
&lt;span class="nv"&gt;After&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;syslog.target network.target

&lt;span class="c"&gt;# 这个 Type=simple 只是 systemd 要如何判断你的服务成功执行&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;Service]
&lt;span class="nv"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;simple
&lt;span class="nv"&gt;WorkingDirectory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/path/to/your/app

&lt;span class="c"&gt;# 如果是用 rbenv:&lt;/span&gt;
&lt;span class="c"&gt;# ExecStart=/bin/bash -lc 'exec /home/deploy/.rbenv/shims/bundle exec sidekiq -e production'&lt;/span&gt;
&lt;span class="c"&gt;# 如果直接用系统安装的 ruby:&lt;/span&gt;
&lt;span class="c"&gt;# ExecStart=/usr/local/bin/bundle exec sidekiq -e production&lt;/span&gt;
&lt;span class="c"&gt;# 如果是用 rvm ，用 ruby 2.6.5 也无特定 gemset&lt;/span&gt;
&lt;span class="c"&gt;# ExecStart=/home/deploy/.rvm/gems/ruby-2.6.5/wrappers/bundle exec sidekiq -e production&lt;/span&gt;
&lt;span class="c"&gt;# 如果是用 rvm ，用 ruby 2.6.5 且有特定 gemset&lt;/span&gt;
&lt;span class="c"&gt;# ExecStart=/home/deploy/.rvm/gems/ruby-2.6.5@gemset-name/wrappers/bundle exec sidekiq -e production&lt;/span&gt;
&lt;span class="c"&gt;# 如果是用 rvm ，用而且专案有 .ruby-version 指定版本&lt;/span&gt;
&lt;span class="nv"&gt;ExecStart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/home/deploy/.rvm/bin/rvm &lt;span class="k"&gt;in&lt;/span&gt; /path/to/your/app/current &lt;span class="k"&gt;do &lt;/span&gt;bundle &lt;span class="nb"&gt;exec &lt;/span&gt;sidekiq &lt;span class="nt"&gt;-e&lt;/span&gt; production

&lt;span class="nv"&gt;User&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;deployer
&lt;span class="nv"&gt;Group&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;deployer
&lt;span class="nv"&gt;UMask&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0002

&lt;span class="c"&gt;# 这行可以大大降低 Ruby memory 用量&lt;/span&gt;
&lt;span class="c"&gt;# 我也是抄来放上&lt;/span&gt;
&lt;span class="c"&gt;# 不过有看 MALLOC_ARENA_MAX=2 的意思是限制 sidekiq 只能用2个 tread_pool &lt;/span&gt;
&lt;span class="c"&gt;# https://www.mikeperham.com/2018/04/25/taming-rails-memory-bloat/&lt;/span&gt;
&lt;span class="nv"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;MALLOC_ARENA_MAX&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2

&lt;span class="c"&gt;# 如果挂掉就重启&lt;/span&gt;
&lt;span class="nv"&gt;RestartSec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;span class="nv"&gt;Restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;on-failure

&lt;span class="c"&gt;# log 会记在 /var/log/syslog&lt;/span&gt;
&lt;span class="nv"&gt;StandardOutput&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;syslog
&lt;span class="nv"&gt;StandardError&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;syslog

&lt;span class="c"&gt;# 这个服务的id 是 sidekiq&lt;/span&gt;
&lt;span class="nv"&gt;SyslogIdentifier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sidekiq

&lt;span class="o"&gt;[&lt;/span&gt;Install]
&lt;span class="nv"&gt;WantedBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;multi-user.target
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;systemd&lt;/code&gt; 会去找 &lt;code&gt;/lib/systemd/system/&lt;/code&gt;下所有的档案，所以 &lt;code&gt;sidekiq.service&lt;/code&gt; 已经可以被 &lt;code&gt;systemd&lt;/code&gt; 找到了。
我们接着可以执行下列指令&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 重读所有服务&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="c"&gt;# enable sidekiq.service 所以它可以在开机后自动运行&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;sidekiq.service
&lt;span class="c"&gt;# 启动 sidekiq&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;service sidekiq start
&lt;span class="c"&gt;# 在 /var/log/syslog 看有没有 sidekiq 的 log&lt;/span&gt;
&lt;span class="nb"&gt;sudo cat&lt;/span&gt; /var/log/syslog
&lt;span class="c"&gt;# 可以用 ps 看 sidekiq 执行了或&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ps aux | &lt;span class="nb"&gt;grep &lt;/span&gt;sidekiq
&lt;span class="c"&gt;# 或直接用 systemctl 来看目前运行中的服务&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="让 Capistrano 重启 Sidekiq"&gt;让 Capistrano 重启 Sidekiq&lt;/h2&gt;
&lt;p&gt;我用 Capistrano 来做自动部署以避免错误。
部署完当然希望 sidekiq 重启，这样它才读到最新的程式码。
要把重启 sidekiq 加到 Capistrano 的流程中，其实只要安装 gem &lt;code&gt;capistrano-sidekiq&lt;/code&gt; 就可以了。
在 Gemfile 加入：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Gemfile&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'capistrano-sidekiq'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;group: :development&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再到 Capfile 加入：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Capfile&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'capistrano/sidekiq'&lt;/span&gt;
&lt;span class="c1"&gt;# 加入 sidekiq 的 rake tasks&lt;/span&gt;
&lt;span class="n"&gt;install_plugin&lt;/span&gt; &lt;span class="no"&gt;Capistrano&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Sidekiq&lt;/span&gt;
&lt;span class="c1"&gt;# 设定要用 systemd 去控制 sidekiq&lt;/span&gt;
&lt;span class="n"&gt;install_plugin&lt;/span&gt; &lt;span class="no"&gt;Capistrano&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Systemd&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样设定好， &lt;code&gt;cap production deploy&lt;/code&gt; 时就会依序执行下列工作：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;停止从 redis 拿工作&lt;/li&gt;
&lt;li&gt;停止 Sidekiq 服务&lt;/li&gt;
&lt;li&gt;开启 Sidekiq 服务&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="做一个一般使用者的 sidekiq.service"&gt;做一个一般使用者的 sidekiq.service&lt;/h2&gt;
&lt;p&gt;上面其实我故意漏说了一件事。
上面新增的 service unit 其实是全系统范围的，也就是要用 &lt;code&gt;sudo&lt;/code&gt; 去执行 &lt;code&gt;systemctl&lt;/code&gt;的
如果我们希望一般的使用者也可以使用 systemd 的话，我们必须要做一个使用者自己的 sidekiq.service
而且 &lt;code&gt;capistrano/sidekiq&lt;/code&gt; 其实预设是要用一般使用者的权限去执行 systemctl 来重启 Sidekiq 的&lt;/p&gt;

&lt;p&gt;当然我们也可以去改 &lt;code&gt;capistrano/sidekiq&lt;/code&gt; 的设定，让它用 root 的权限去操作 systemctl，我只是想说明其实有多种选择，每种都可以。而且一开始我的 &lt;code&gt;sidekiq.service&lt;/code&gt; 怎麽都跑不起来就是因为我没注意到这件事....所以想特别提一下。&lt;/p&gt;

&lt;p&gt;If you have already enabled the system-wise &lt;code&gt;sidekiq.service&lt;/code&gt;, you need to disable it and delete the service-unit:
如果你刚刚已经安装好了全系统级别的 sidekiq.servive，你需要执行下面步骤去取消它：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 先停止 sidekiq&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl stop sidekiq
&lt;span class="c"&gt;# 不要启用 sidekiq.service&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl disable sidekiq.service
&lt;span class="c"&gt;# 删掉&lt;/span&gt;
&lt;span class="nb"&gt;sudo rm&lt;/span&gt; /lib/systemd/system/sidekiq.service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一般使用者级别的服务单元要放在 &lt;code&gt;~/.config/systemd/user/&lt;/code&gt; 下，systemd 会去检查下面的档案。我们再新增一次 &lt;code&gt;sidekiq.service&lt;/code&gt;，它稍有一些不同：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;Unit]
&lt;span class="nv"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sidekiq
&lt;span class="nv"&gt;After&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;syslog.target network.target

&lt;span class="o"&gt;[&lt;/span&gt;Service]
&lt;span class="c"&gt;# 如果是用 Sidekiq 6.0.6 以后的版本，可以改成 notify 并配合 WatchdogSec&lt;/span&gt;
&lt;span class="c"&gt;# 否则沿用 simple&lt;/span&gt;
&lt;span class="nv"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;notify
&lt;span class="nv"&gt;WatchdogSec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10

&lt;span class="nv"&gt;WorkingDirectory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/path/to/your/app/current

&lt;span class="c"&gt;# 如果是用 rbenv:&lt;/span&gt;
&lt;span class="c"&gt;# ExecStart=/bin/bash -lc 'exec /home/deploy/.rbenv/shims/bundle exec sidekiq -e production'&lt;/span&gt;
&lt;span class="c"&gt;# 如果直接用系统安装的 ruby:&lt;/span&gt;
&lt;span class="c"&gt;# ExecStart=/usr/local/bin/bundle exec sidekiq -e production&lt;/span&gt;
&lt;span class="c"&gt;# 如果是用 rvm ，用 ruby 2.6.5 也无特定 gemset&lt;/span&gt;
&lt;span class="c"&gt;# ExecStart=/home/deploy/.rvm/gems/ruby-2.6.5/wrappers/bundle exec sidekiq -e production&lt;/span&gt;
&lt;span class="c"&gt;# 如果是用 rvm ，用 ruby 2.6.5 且有特定 gemset&lt;/span&gt;
&lt;span class="c"&gt;# ExecStart=/home/deploy/.rvm/gems/ruby-2.6.5@gemset-name/wrappers/bundle exec sidekiq -e production&lt;/span&gt;
&lt;span class="c"&gt;# 如果是用 rvm ，用而且专案有 .ruby-version 指定版本&lt;/span&gt;
&lt;span class="nv"&gt;ExecStart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/home/deploy/.rvm/bin/rvm &lt;span class="k"&gt;in&lt;/span&gt; /path/to/your/app/current &lt;span class="k"&gt;do &lt;/span&gt;bundle &lt;span class="nb"&gt;exec &lt;/span&gt;sidekiq &lt;span class="nt"&gt;-e&lt;/span&gt; production
&lt;span class="nv"&gt;ExecReload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/bin/kill &lt;span class="nt"&gt;-TSTP&lt;/span&gt; &lt;span class="nv"&gt;$MAINPID&lt;/span&gt;

&lt;span class="c"&gt;# 这行可以大大降低 Ruby memory 用量&lt;/span&gt;
&lt;span class="c"&gt;# 我也是抄来放上&lt;/span&gt;
&lt;span class="c"&gt;# 不过有看 MALLOC_ARENA_MAX=2 的意思是限制 sidekiq 只能用2个 tread_pool &lt;/span&gt;
&lt;span class="c"&gt;# https://www.mikeperham.com/2018/04/25/taming-rails-memory-bloat/&lt;/span&gt;
&lt;span class="nv"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;MALLOC_ARENA_MAX&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2

&lt;span class="c"&gt;# 如果挂掉就重启&lt;/span&gt;
&lt;span class="nv"&gt;RestartSec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;span class="nv"&gt;Restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;on-failure

&lt;span class="c"&gt;# log 会记在 /var/log/syslog&lt;/span&gt;
&lt;span class="nv"&gt;StandardOutput&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;syslog
&lt;span class="nv"&gt;StandardError&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;syslog

&lt;span class="c"&gt;# 这个服务的id 是 sidekiq&lt;/span&gt;
&lt;span class="nv"&gt;SyslogIdentifier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sidekiq

&lt;span class="o"&gt;[&lt;/span&gt;Install]
&lt;span class="nv"&gt;WantedBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;default.target
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们再跑以下指令来启用/启动 &lt;code&gt;sidekiq.service&lt;/code&gt;:&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; daemon-reload 
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nb"&gt;enable &lt;/span&gt;sidekiq.service

&lt;span class="c"&gt;# 现在可以控制 sidekiq 啦&lt;/span&gt;
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;start,stop,restart&lt;span class="o"&gt;}&lt;/span&gt; sidekiq
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在 &lt;code&gt;capistrano/sidekiq&lt;/code&gt; 应该可以正常运作了，用 capistrano 部署看看吧 : )&lt;/p&gt;
&lt;h2 id="其它问题"&gt;其它问题&lt;/h2&gt;&lt;h3 id="部署时跑 sidekiq:quiet 出现以下错误"&gt;部署时跑 sidekiq:quiet 出现以下错误&lt;/h3&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sidekiq:quiet
    01 systemctl --user reload sidekiq
    01 Failed to reload sidekiq.service: Job type reload is not applicable for unit sidekiq.service.
  ✘ 01 deployer@xxxxxxxx 0.067s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解法
    * 加  &lt;code&gt;ExecReload=/bin/kill -TSTP $MAINPID&lt;/code&gt; 进 sidekiq.service&lt;/p&gt;
&lt;h3 id="出现 target 找不到"&gt;出现 target 找不到&lt;/h3&gt;
&lt;p&gt;* target 其实是一组 service unit，执行 target 时全部服务会一起执行
    * 用 &lt;code&gt;systemctl list-units —type=target&lt;/code&gt; 去看能用的 target&lt;/p&gt;
&lt;h3 id="-L log/sidekiq.log 无效"&gt;-L log/sidekiq.log 无效&lt;/h3&gt;
&lt;p&gt;* Sidekiq 6 不再支援 log 的转向，请看 &lt;a href="https://github.com/mperham/sidekiq/wiki/Logging" rel="nofollow" target="_blank" title=""&gt;Logging · mperham/sidekiq Wiki · GitHub&lt;/a&gt;，一律看 /var/log/syslog
    * 可以用 &lt;code&gt;bundle exec sidekiq 2&amp;gt;&amp;amp;1 | logger -t sidekiq&lt;/code&gt; 为 log 加上 sidekiq 的 tag&lt;/p&gt;
&lt;h2 id="参考资料"&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;其实 &lt;code&gt;sidekiq.service&lt;/code&gt; 大部分我也是从 sidekiq repo 看来的啦：&lt;a href="https://github.com/mperham/sidekiq/blob/master/examples/systemd/sidekiq.service" rel="nofollow" target="_blank" title=""&gt;sidekiq/sidekiq.service at master · mperham/sidekiq · GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/seuros/capistrano-sidekiq" rel="nofollow" target="_blank" title=""&gt;GitHub - seuros/capistrano-sidekiq: Sidekiq integration for Capistrano&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>kevinluo201</author>
      <pubDate>Sat, 21 Aug 2021 01:06:16 +0800</pubDate>
      <link>https://ruby-china.org/topics/41600</link>
      <guid>https://ruby-china.org/topics/41600</guid>
    </item>
    <item>
      <title>用 Github Actions 持续整合 Rails</title>
      <description>&lt;p&gt;dev.to 版
&lt;a href="https://dev.to/kevinluo201/github-actions-ruby-on-rails-42ig" rel="nofollow" target="_blank" title=""&gt;https://dev.to/kevinluo201/github-actions-ruby-on-rails-42ig&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;最近研究了一下用 Github Actions 做 Rails 的 CI，分享一下经验 : )&lt;/p&gt;

&lt;p&gt;Github Actions 是 Github 的自动化工具。
Github Actions 只要在你的专案根目录新增 &lt;code&gt;.github/workflows&lt;/code&gt; ，再新增任意名称的 Yaml 档。
Git push 到 Github 后，即会根据你写的 workflow 的内容自动执行了。&lt;/p&gt;

&lt;p&gt;可以直接看下面分享的 YAML 档，不过建议还是先看一下 Github Action 的文档 &lt;a href="https://docs.github.com/en/actions/learn-github-actions/introduction-to-github-actions" rel="nofollow" target="_blank" title=""&gt;Introduction to GitHub Actions - GitHub Docs&lt;/a&gt; ，会比较有概念喔 (常常更新的也满快的)&lt;/p&gt;

&lt;p&gt;先分享 &lt;code&gt;.github/workflows/ci.yml&lt;/code&gt;，后面会再说明每一列是代表什麽意思&lt;/p&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CI&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v1&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup Ruby&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ruby/setup-ruby@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ruby-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2.7.2&lt;/span&gt;
          &lt;span class="na"&gt;bundler-cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Node&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;12.16.x'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Restore cached ./node_modules&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/cache@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./node_modules&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ runner.os }}-yarn-lock-${{ hashFiles('./yarn.lock') }}&lt;/span&gt;
          &lt;span class="na"&gt;restore-keys&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;${{ runner.os }}-yarn-lock-&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Yarn Install&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn install&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;mysql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mysql:8&lt;/span&gt;
        &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3306:3306'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;MYSQL_ROOT_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;my-root-pw'&lt;/span&gt;
          &lt;span class="na"&gt;MYSQL_DATABASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test_db&lt;/span&gt;
          &lt;span class="na"&gt;MYSQL_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;username_you_like&lt;/span&gt;
          &lt;span class="na"&gt;MYSQL_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;password_you_like&lt;/span&gt;
        &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;--health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3&lt;/span&gt;
      &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis&lt;/span&gt;
        &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;6379:6379'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;--entrypoint redis-server&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v1&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup Ruby&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ruby/setup-ruby@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ruby-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2.7.2&lt;/span&gt;
          &lt;span class="na"&gt;bundler-cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Node&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;12.16.x'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Restore cached ./node_modules&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/cache@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./node_modules&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ runner.os }}-yarn-lock-${{ hashFiles('./yarn.lock') }}&lt;/span&gt;
          &lt;span class="na"&gt;restore-keys&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;${{ runner.os }}-yarn-lock-&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Yarn Install&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn install&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Prepare Database&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;RAILS_ENV&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
          &lt;span class="na"&gt;RAILS_MASTER_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.RAILS_MASTER_KEY }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bundle exec rails db:prepare&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run tests&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;REDIS_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis://localhost:6379/1&lt;/span&gt;
          &lt;span class="na"&gt;RAILS_ENV&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
          &lt;span class="na"&gt;RAILS_MASTER_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.RAILS_MASTER_KEY }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;bundle exec rspec --format RspecJunitFormatter --out ./reports/rspec.xml&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Publish Test Report&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mikepenz/action-junit-report@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;report_paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;./reports/rspec.xml'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="说明"&gt;说明&lt;/h2&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CI&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此 workflow 的名称，可任意取，到时会出现在 Github Actions 中&lt;/p&gt;

&lt;hr&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;on 是控制何时要执行这个 workflow&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;push: 有 commit  push 时&lt;/li&gt;
&lt;li&gt;pull_request: PR 更新时&lt;/li&gt;
&lt;/ul&gt;

&lt;hr&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; 
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个 workflow 可以有很多 jobs 组成，它们通常是同时 (平行) 执行的，
不过可以设定先后顺序，下面一点会提到。
job 的名称可任意取名，我这裡叫它 build
&lt;code&gt;runs-on&lt;/code&gt;  是 job 要在什麽 OS 上执行，github 上的 Github Actions 好像得用 ubuntu-latest，我也是照教学沿用了&lt;/p&gt;

&lt;hr&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;step&lt;/code&gt; 是 Github Actions 的最小单位
steps 裡每个 step 可以执行一或多个指令或一个 Action
要执行指令，则使用 &lt;code&gt;run&lt;/code&gt;
  &lt;code&gt;run: echo "hello world!"&lt;/code&gt;
要执行 Action，用 &lt;code&gt;uses&lt;/code&gt;
  &lt;code&gt;uses: actions的名字&lt;/code&gt;
每个 step 的 name 并不是必填，但填了就像註解一样，方便理解&lt;/p&gt;

&lt;hr&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Action 其实就预先写好的脚本
GitHub Marketplace 上有一堆，可以想像是想把自动化的脚本当成在 App Store 上卖
&lt;a href="https://github.com/marketplace?type=actions" rel="nofollow" target="_blank" title=""&gt;GitHub Marketplace · Actions to improve your workflow · GitHub&lt;/a&gt;
&lt;code&gt;actions/checkout@v1&lt;/code&gt; 就是一个常用的 Action，可以把你的 git repository 下载下来&lt;/p&gt;

&lt;hr&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup Ruby&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ruby/setup-ruby@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ruby-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2.7.2&lt;/span&gt;
    &lt;span class="na"&gt;bundler-cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ruby/setup-ruby@v1 是用来安装 Ruby 的 Action
下载的 ruby-version: 2.7.2 是要装 2.7.2 的 Ruby
 bundler-cache: true 是要快取 bundler 下载的 gem，下面再说明快取
基本上这些 Action 都可以在 Github Marketplace 上查到用法
 &lt;a href="https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby" rel="nofollow" target="_blank" title=""&gt;Setup Ruby, JRuby and TruffleRuby · Actions · GitHub Marketplace · GitHub&lt;/a&gt;&lt;/p&gt;

&lt;hr&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Node&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v2&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;12.16.x'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同上，安装 Node&lt;/p&gt;

&lt;hr&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Restore cached ./node_modules&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/cache@v2&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./node_modules&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ runner.os }}-yarn-lock-${{ hashFiles('./yarn.lock') }}&lt;/span&gt;
    &lt;span class="na"&gt;restore-keys&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;${{ runner.os }}-yarn-lock-&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Yarn Install&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;actions/cache@v2&lt;/code&gt; 是专门来做快取的 Action
这裡写 path: ./node_modules 就是要把 ./node_modules 下的档案全 cache 起来
快取的结果会用 key 的设定去命名。
这个 action 会先依照 restore-keys 去找可以回復的快取
最后的 step 纯粹就是跑 yarn install&lt;/p&gt;

&lt;p&gt;如果光看这个 顺序应该有人会疑惑：
快取的 action 是放在 yarn install 前，或更上方 ruby 的部分有 bundle install 前，
在安装依赖前，先「读取」快取好理解，
但重要的是「存」快取的时机怎麽没看到？
执行一次就知道，这类快取的 Action 都有挂个动作在 PostJob 的 callback，
当这个 job 结束后会把该快取的 path 存起来&lt;/p&gt;

&lt;p&gt;另外，每个仓库有 5GB 的快取空间，正常来说应该是用不完啦。
所以 gems, npm 这种安装的程式库都把它们快取起来吧&lt;/p&gt;

&lt;hr&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build&lt;/span&gt;
  &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是第二个 job，我命名为 test，因为准备要跑 rspec 了
上面说 job 其实是平行执行的，&lt;code&gt;needs&lt;/code&gt; 可以控制先后顺序，needs: build 的意思就是 build 跑完才会跑 test
虽然以目前这个 workflow 来看，其实可以把 steps 全写在同一个 job，没什麽差别。
但写成这样，可以方便未来加入不同类的 test，比如 js 的 test，capybara 的 test，可以在 build 后，所有的 test 同时执行。&lt;/p&gt;

&lt;hr&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mysql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mysql:8&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3306:3306'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_ROOT_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;my-root-pw'&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_DATABASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test_db&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test_user&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test_pw&lt;/span&gt;
    &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;--health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3&lt;/span&gt;
  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;6379:6379'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;--entrypoint redis-server&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;services&lt;/code&gt; 可以用 docker image 架起需要用的服务，设定好 port  mapping
我这裡架了 mysql 和 redis
那些 env 是去 DockerHub 上去找官方的 image 的说明才知道有什麽可以加的
顺带一提，CI 时 &lt;code&gt;config/database.yml&lt;/code&gt; 可以两种处理方式&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;直接加入 git，内容再用 ENV 去替换&lt;/li&gt;
&lt;li&gt;新增一个 database.yml.ci，在 workflow 裡加一个 step 去 &lt;code&gt;run: cp config/database.yml.ci config/database.yml&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;hr&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v1&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup Ruby&lt;/span&gt;
    &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ruby/setup-ruby@v1&lt;/span&gt;
    &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;ruby-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2.7.2&lt;/span&gt;
      &lt;span class="na"&gt;bundler-cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Node&lt;/span&gt;
    &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v2&lt;/span&gt;
    &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;12.16.x'&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Restore cached ./node_modules&lt;/span&gt;
    &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/cache@v2&lt;/span&gt;
    &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./node_modules&lt;/span&gt;
      &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ runner.os }}-yarn-lock-${{ hashFiles('./yarn.lock') }}&lt;/span&gt;
      &lt;span class="na"&gt;restore-keys&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;${{ runner.os }}-yarn-lock-&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Yarn Install&lt;/span&gt;
    &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这裡跟 build 是做完全一模一样的事，执行时会直接取 build 快取的结果，所以很快就会跑完。
看起来很冗，不过如果未来可以用 YAML 的 Anchor 功能，这一块可以直接写成一个可複用的 block，目前就直接重複吧！&lt;/p&gt;

&lt;hr&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Prepare Database&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;RAILS_ENV&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
    &lt;span class="na"&gt;RAILS_MASTER_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.RAILS_MASTER_KEY }}&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bundle exec rails db:prepare&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要开始跑 rails 的指令时，要带一些&lt;code&gt;环境变数&lt;/code&gt;时，例如我这样会用 RAILS_ENV 跟 RAILS_MASTER_KEY
是密码类的字串可以存在 Github 仓库 &amp;gt;Settings&amp;gt;Secrets 裡，workflow 裡可以直接 &lt;code&gt;secrets.RAILS_MASTER_KEY&lt;/code&gt; 去读取
&lt;img src="https://l.ruby-china.com/photo/kevinluo201/5675aa94-88e1-45b5-9ef7-01e316ac0286.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;hr&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run tests&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;REDIS_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis://localhost:6379/1&lt;/span&gt;
    &lt;span class="na"&gt;RAILS_ENV&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
    &lt;span class="na"&gt;RAILS_MASTER_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.RAILS_MASTER_KEY }}&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bundle exec rspec --format RspecJunitFormatter --out ./reports/rspec.xml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这边是跑 rspec
有装 rspec_junit-formatter，所以可以产生一个 XML 档纪录测试结果&lt;/p&gt;

&lt;hr&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- name: Publish Test Report
  uses: mikepenz/action-junit-report@v2
  with:
    report_paths: './reports/rspec.xml'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后用 &lt;code&gt;mikepenz/action-junit-report@v2&lt;/code&gt;
去读取 XML 的测试结果并秀出在页面上
&lt;img src="https://l.ruby-china.com/photo/kevinluo201/a0709953-95d7-4bea-a73e-37f31c134f62.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="心得"&gt;心得&lt;/h2&gt;
&lt;p&gt;我觉得自动化最烦的就是一堆小细节要顾，
比如说也可以完全不快取硬让它跑，但就会很慢。
像 CircleCI 的 &lt;code&gt;orb&lt;/code&gt; 就是把每个语言、框架常用的自动化指令整理起来变成一组指令集。
但 Github Actions 直接更进一步让社群製作指令集，并准备好市集，方便搜寻跟分享，野心勃勃要干掉其它 CI。&lt;/p&gt;

&lt;p&gt;整体上还满容易用，连 Rails 都能轻易导入 (回想一下把 Rails 装进 Docker 的夜晚)，满推荐试试看的。
而且 Github(微软) 满佛的，每个月给 2000 小时，个人练习或小型专案应该是用不完啦，可以去帐号 Billing &amp;amp; Plans 下查用量。
&lt;img src="https://l.ruby-china.com/photo/kevinluo201/44185325-f875-4899-8de3-fe9e7a4c3f70.png!large" title="" alt=""&gt;&lt;/p&gt;
&lt;h2 id="参考资料"&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://gorails.com/episodes/github-actions-continuous-integration-ruby-on-rails?autoplay=1" rel="nofollow" target="_blank" title=""&gt;GitHub Actions with Ruby on Rails: Setting up Continuous Integration (Example) | GoRails&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/actions/learn-github-actions/introduction-to-github-actions" rel="nofollow" target="_blank" title=""&gt;Introduction to GitHub Actions - GitHub Docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>kevinluo201</author>
      <pubDate>Tue, 22 Jun 2021 01:00:28 +0800</pubDate>
      <link>https://ruby-china.org/topics/41394</link>
      <guid>https://ruby-china.org/topics/41394</guid>
    </item>
    <item>
      <title>partial 的救赎？介绍 view_component</title>
      <description>&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/kevinluo201/982514db-629f-4fe1-bc46-296968cefef2.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;把前端封装 (encapsulation) 是从 React.js 以来的趋势，
Github 前年受 React 启發表了一个 view_component 的 gem，并用在 Github 裡。&lt;/p&gt;

&lt;p&gt;Rails 的开發者应该本来就有把有複用的页面元素抽出来变成 partial 的习惯，
不过 view_component 想要解决 partail 常遇到的 3 种问题：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Partial render 的速度慢&lt;/li&gt;
&lt;li&gt;Partial 裡常有天外飞来的 instance variable 或 helper&lt;/li&gt;
&lt;li&gt;Partial 不好写單元测试&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;最近刚好研究了一下 view_component，分享心得。&lt;/p&gt;
&lt;h2 id="References"&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://viewcomponent.org/" rel="nofollow" target="_blank" title=""&gt;view_component | A framework for building reusable, testable &amp;amp; encapsulated view components in Ruby on Rails.&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=y5Z5a6QdA-M" rel="nofollow" target="_blank" title=""&gt;RailsConf 2019 - Rethinking the View Layer with Components by Joel Hawksley&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.blog/2020-12-15-encapsulating-ruby-on-rails-views/" rel="nofollow" target="_blank" title=""&gt;Encapsulating Ruby on Rails views - The GitHub Blog&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="使用方式"&gt;使用方式&lt;/h2&gt;
&lt;p&gt;在 Gemfile 中加入 view_component&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Gemfile&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s2"&gt;"view_component"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;require: &lt;/span&gt;&lt;span class="s2"&gt;"view_component/engine"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装完后，新增一个 ExampleComponent&lt;br&gt;
&lt;code&gt;$ bundle exec rails g component ExampleComponent greeting&lt;/code&gt;&lt;br&gt;
这样会新增 3 个档案：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;app/components/example_component.rb&lt;/li&gt;
&lt;li&gt;app/components/example_component.html.erb&lt;/li&gt;
&lt;li&gt;test/components/exmample_component_test.rb
把内容先改成这样&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# app/components/example_component.rb
# frozen_string_literal: true
class ExampleComponent &amp;lt; ViewComponent::Base
  def initialize(greeting:)
    @greeting = greeting
  end
end


# app/components/example_component.html.erb
&amp;lt;div&amp;gt;
  &amp;lt;span&amp;gt;&amp;lt;%= @greeting %&amp;gt;&amp;lt;/span&amp;gt;
&amp;lt;/div&amp;gt;


# test/components/example_component_test.rb
require "test_helper"
class ExampleComponentTest &amp;lt; ViewComponent::TestCase
  def test_component_rendering
    assert_equal(
      %(&amp;lt;span&amp;gt;Hello!&amp;lt;/span&amp;gt;),
      render_inline(ExampleComponent.new(greeting: "Hello!")).css("span").to_html
    )
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后跟的用法跟 partial 很像，将 instance 丢入  render 即可&lt;/p&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="no"&gt;ExampleComponent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;greeting: &lt;/span&gt;&lt;span class="s1"&gt;'hi'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;即会将 erb 的渲染至网页中&lt;/p&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;hi&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可利用&lt;code&gt;bundle rails test test/components/example_component_test.rb&lt;/code&gt;&lt;br&gt;
对 ExampleComponent 单独做 Unit Test&lt;br&gt;&lt;/p&gt;

&lt;p&gt;大致上最简单的用法就是这样了&lt;/p&gt;
&lt;h2 id="速度"&gt;速度&lt;/h2&gt;
&lt;p&gt;可以直接用 benchmark 来测速一下
我用的环境是&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ruby 2.7.2p137&lt;/li&gt;
&lt;li&gt;Rails 6.1.3&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;用 partial 档案 _hi.html.erb 跟上述的 ExampleComponent 来比较&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# app/views/pages/_hi.html.erb
&amp;lt;span&amp;gt;hi!&amp;lt;/span&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比较一下 3 种方式都给它印 10000 次 &lt;code&gt;hi&lt;/code&gt; &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;直接写在 erb 中就称为 inline&lt;/li&gt;
&lt;li&gt;partial&lt;/li&gt;
&lt;li&gt;component &lt;/li&gt;
&lt;/ol&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'benchmark'&lt;/span&gt;
   &lt;span class="no"&gt;Benchmark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bmbm&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
     &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;report&lt;/span&gt; &lt;span class="s2"&gt;"inline"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
       &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
         &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;hi&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;
     &lt;span class="k"&gt;end&lt;/span&gt;
     &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;report&lt;/span&gt; &lt;span class="s2"&gt;"partial"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
       &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="s2"&gt;"hi"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;
     &lt;span class="k"&gt;end&lt;/span&gt;
     &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;report&lt;/span&gt; &lt;span class="s2"&gt;"component"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
       &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="no"&gt;ExampleComponent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;greeting: &lt;/span&gt;&lt;span class="s1"&gt;'hi'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;
     &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;-%&amp;gt;&lt;/span&gt;

#结果(秒为单位)
#inline      0.002143   0.000183   0.002326  (  0.002353)
#partial     78.692460  0.785214   79.477674 ( 80.162131)
#component   0.061728   0.000816   0.062544  (  0.062694)
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Inline 是最快 2ms,  平均 0.0002ms/次&lt;/li&gt;
&lt;li&gt;partial 花了 80 秒，平均 8ms / 次&lt;/li&gt;
&lt;li&gt;Component  62ms, 平均 0.0062ms / 次
Partial 虽然最慢，但其实 8ms 也是满快的
不过 view_component 好像快太多了...快 1000 倍 xD&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这是最简单的测试，
view_component 的官网上是说比 partial 快 10 倍以上&lt;/p&gt;

&lt;p&gt;祕密应该就是在 view_component 改写了 render 的方法，
略过一堆找 template 的过程，而且有加 cache，
所以上面的实验才会快那麽多&lt;/p&gt;
&lt;h2 id="资料流"&gt;资料流&lt;/h2&gt;
&lt;p&gt;Component 很明确限制只能使用在 Component Class 内定义的变数跟方法。
instance_variable 跟 helper 的方法在 erb 中可以当成是全域的。
用 partial 常投机狂用 instance variable 而不是 locals，造成维护困难
大大减少非预期的內容，比较好追 code&lt;/p&gt;
&lt;h2 id="测试"&gt;测试&lt;/h2&gt;
&lt;p&gt;Rails 的 MVC 架构的 model 跟 controller 都很容易来写测试，
唯独 view 的不是很好写 test，但偏偏 view 又是佔大量程式码的部分，
造成 test coverage 普遍不高。
partial 更是不能单独测试，一定是配合 controller 真的画出整个网页的 html 或 end2end 的 system test。
ViewComponent 可以在测试 中渲染单一 component，即解决 partial 无法 unit test 的问题&lt;/p&gt;
&lt;h2 id="其它用法"&gt;其它用法&lt;/h2&gt;&lt;h2 id="Slot"&gt;Slot&lt;/h2&gt;
&lt;p&gt;如果有写 Vue.js，对 Slot 的概念应该是不陌生，就是可从 template 外部塞入 html。
Slot 设定方法很多种，下面用简单的方式加入一个 header slot&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/components/example_component.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ExampleComponent&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ViewComponent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;ViewComponent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;SlotableV2&lt;/span&gt;
  &lt;span class="n"&gt;renders_one&lt;/span&gt; &lt;span class="ss"&gt;:header&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;greeting&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="vi"&gt;@greeting&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;greeting&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# app/components/example_component.rb&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;%= header %&amp;gt;
  &amp;lt;span&amp;gt;&amp;lt;%=&lt;/span&gt; &lt;span class="vi"&gt;@greeting&lt;/span&gt; &lt;span class="o"&gt;%&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/span&amp;gt;
&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用时，在 render 后带入一个 block，并在指定 slot 中放入一个 Home 的联结：&lt;/p&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="no"&gt;ExampleComponent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;greeting: &lt;/span&gt;&lt;span class="s1"&gt;'hi'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s2"&gt;"Home"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;root_path&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果就是 Home 的联结会加入 header slot 的位置&lt;/p&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Home&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;hi&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="Validations"&gt;Validations&lt;/h2&gt;
&lt;p&gt;可以加入 ActiveModel::Validations 来强迫 Component  一定要有某些 Slot 或传入变数的形式，避免错误。
或者是说可以抛出错误，帮助查错。&lt;/p&gt;
&lt;h2 id="Sidecar(实验中)"&gt;Sidecar(实验中)&lt;/h2&gt;
&lt;p&gt;Component 可以有自己 scoped 的 css, js，这样就功能就可能可以跟前端框架比一比了。&lt;/p&gt;
&lt;h2 id="preview"&gt;preview&lt;/h2&gt;
&lt;p&gt;前端自动测试常有个问题：测试都过，所有的行为都对了，但是真的打开浏览器时，显示错了。
目前最好的方式可能是靠人眼去看元件渲染出来的结果。
&lt;code&gt;test/components/previews/example_component_preview.rb&lt;/code&gt;&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ExampleComponentPreview&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ViewComponent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Preview&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;with_default_greeting&lt;/span&gt;
    &lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;ExampleComponent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;greeting: &lt;/span&gt;&lt;span class="s2"&gt;"Example component default"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;with_long_text&lt;/span&gt;
    &lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;ExampleComponent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;greeting: &lt;/span&gt;&lt;span class="s2"&gt;"This is a really long title to see how the component renders this"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以利用产生出的路径
&lt;code&gt;http://localhost:3000/rails/view_components/example_component/with_default_title&lt;/code&gt;
就可以直接点进去看渲染出的元件了&lt;/p&gt;

&lt;p&gt;对前端熟的人可能会知道 Storybook.js 这个框架，
一个 ViewComponent 用的 storybook 的 gem 已经有人开發了
&lt;a href="https://github.com/jonspalmer/view_component_storybook" rel="nofollow" target="_blank" title=""&gt;GitHub - jonspalmer/view_component_storybook: ViewComponent previews and testing in Storybook&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="小结"&gt;小结&lt;/h2&gt;
&lt;p&gt;虽然较进阶的用法还在实验阶段，
但已可看的出 Github 对他们这个设计非常满意 😂
Github 的人为了 view_component，
已在在 Rails 6.1 加入允许物件实作自己的 render 的方式：
只要该物件有&lt;code&gt;render_in&lt;/code&gt; 的方法，render 时就会改去呼叫物件的 render_in。
完全为 view_component 量身打造，不用再去 monkey patch ActionView 的 render。&lt;/p&gt;

&lt;p&gt;而且已经把非常多元件做成 ViewComponent 的形式，&lt;br&gt;
打算做成一个元件库 &lt;a href="https://primer.style/view-components/" rel="nofollow" target="_blank" title=""&gt;Primer ViewComponents&lt;/a&gt;&lt;br&gt;
还在 Beta，但看来很完整，应该不久后就正式推出了 (？)&lt;/p&gt;

&lt;p&gt;我个人是觉得很值得尝试用来取代 partial，
但因为跟前端框架 React, Vue 来比较时，view_component 没有天生支援 track 状态变化，
可能等 Sidecar 的功能正式推出后，会有更好的做法。&lt;/p&gt;

&lt;p&gt;(不过也许跟 DHH 主推的 stimulus.js 或 Hotwire 配合会有奇效喔。)&lt;/p&gt;</description>
      <author>kevinluo201</author>
      <pubDate>Sun, 14 Mar 2021 18:51:17 +0800</pubDate>
      <link>https://ruby-china.org/topics/41027</link>
      <guid>https://ruby-china.org/topics/41027</guid>
    </item>
    <item>
      <title>Web 的摩斯密码，Base64 介绍</title>
      <description>&lt;p&gt;本来写在 &lt;a href="https://ruby-china.org/topics/40933" title=""&gt;JWT 介绍&lt;/a&gt;&lt;br&gt;
但好像太长了，拆出来分享&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;因为看到 ruby3 的 release note 裡特别有讲把 base64 放上 rubygems.org&lt;br&gt;
&lt;a href="https://github.com/ruby/base64" rel="nofollow" target="_blank" title=""&gt;https://github.com/ruby/base64&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;也让我想到好像至今没有真的了解 base64 在做什麽...。&lt;br&gt;
就是每次处理非纯文字的资料时，&lt;br&gt;
都是把档案转成 base64 的字串表示。&lt;br&gt;
比如在 rails 上传图片时，图片在 &lt;code&gt;params&lt;/code&gt; 中就是 base64 的形式，&lt;br&gt;
而且另个疑问是，为什麽要称那些资料为 Binary data 呢？&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;预设你已了解 bit 跟 byte 还有字的编码如 ASCII，读起来较易理解&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="参考资料"&gt;参考资料&lt;/h2&gt;
&lt;p&gt;当然是 Wiki &lt;a href="https://en.wikipedia.org/wiki/Base64" rel="nofollow" target="_blank" title=""&gt;Base64 - Wikipedia&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="base64"&gt;base64&lt;/h2&gt;
&lt;p&gt;base64 可以想成「用 64 种字元」去代表所有的资料的一种方式&lt;/p&gt;

&lt;p&gt;因为电脑其实只认 0 跟 1&lt;br&gt;
所以资料不论是何种类型，到最后一定会变成是 0 跟 1 的组合。例如：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;数字 6 会转成二进位的 110&lt;/li&gt;
&lt;li&gt;字元 A 是 100 0001(因为 ASCII 裡 A=65，而 65 的二进位是 100 0001)&lt;/li&gt;
&lt;li&gt;字串是字元连起来的，所以是一长串的 01 组合&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;base64 是把 01 这种二进位的资料，以 6 个为一组呈现的方式&lt;br&gt;
任 6 位数 2 进位，如 &lt;code&gt;000 001&lt;/code&gt;, &lt;code&gt;001 101&lt;/code&gt;&lt;br&gt;
总共会有 64 种组合，所以命名为 base64&lt;/p&gt;

&lt;p&gt;给每种组合一个代表的「字元 Char」，&lt;br&gt;
就可以用那 64 种字元去代表所有的字串了，&lt;br&gt;
(如果 bits 数不能被 6 整除，会用 &lt;code&gt;=&lt;/code&gt; 去代替)&lt;br&gt;
下表是 wiki 上列出这 64 个字元：
&lt;img src="https://l.ruby-china.com/photo/kevinluo201/5f10d70d-d52e-41d3-86c5-baa721cf5e06.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;以 Wiki 页 上的例子 &lt;code&gt;Man&lt;/code&gt; 这个字串来说，&lt;br&gt;
ASCII 是用 8 bits 存的，所以 共有 24 bits&lt;br&gt;
转成 base64：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;M 是 010011 01, 前面 6 bit  010011 依上表 是 &lt;code&gt;T&lt;/code&gt; ，然后剩 2  bit &lt;code&gt;01&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;a 是 0110 0001，前面的 4 bit 配上 M 后 2 bit, 010110 是 &lt;code&gt;W&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;依续做可得 &lt;code&gt;TWFu&lt;/code&gt;
&lt;img src="https://l.ruby-china.com/photo/kevinluo201/4c5ed2db-c7b5-4188-8961-19e2b872d2d8.png!large" title="" alt=""&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;我们可以用这个方式拆解中文&lt;code&gt;我&lt;/code&gt;试看看：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="s1"&gt;'我'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unpack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"B*"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# 将 String 转二进位&lt;/span&gt;
&lt;span class="c1"&gt;# "111001101000100010010001"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;我&lt;/code&gt;刚好是 3 个 byte 也就是 24bits 所以用 base64 恰好是 4 个字元显示 
&lt;img src="https://l.ruby-china.com/photo/kevinluo201/0878062a-6bb4-4c36-a383-663863f66cd4.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;实际操作看看：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'base64'&lt;/span&gt;
&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="no"&gt;Base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'我'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# 5oiR&lt;/span&gt;

&lt;span class="c1"&gt;# 注意decode base64时，它不知道那是 utf-8 所以要自己转回去&lt;/span&gt;
&lt;span class="no"&gt;Base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'5oiR'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# "\xE6\x88\x91" &lt;/span&gt;
&lt;span class="no"&gt;Base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'5oiR'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;encoding&lt;/span&gt;
&lt;span class="c1"&gt;# #&amp;lt;Encoding:ASCII-8BIT&amp;gt; &lt;/span&gt;
&lt;span class="no"&gt;Base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'5oiR'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;force_encoding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'UTF-8'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# "我"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而那些根本不是任何「文字」的档案，比如说图片、音乐、影片，&lt;br&gt;
根本不能转成文字，但依然可以以 0 跟 1 表示，&lt;br&gt;
可以写成超长的&lt;code&gt;011010101010....&lt;/code&gt;的字串，&lt;br&gt;
就称它们为 &lt;strong&gt;Binary data&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;在传送档案时，就可以用 base64 去表示任何档案，&lt;br&gt;
又因为 base64 转回来时只是一般的 ASCII，所以才为什麽一定要加註&lt;br&gt;
Content-Type 或副档名。&lt;/p&gt;
&lt;h2 id="Q&amp;amp;A"&gt;Q&amp;amp;A&lt;/h2&gt;&lt;h2 id="base64 最终还不是会换为 0 与 1，为什麽不直接传 01 就好了？"&gt;base64 最终还不是会换为 0 与 1，为什麽不直接传 01 就好了？&lt;/h2&gt;
&lt;p&gt;&lt;del&gt;主要原因还是希望传输的内容人易看懂，即使 base64 是不可读的，但英数真的比较容易分辨 2 串字一不一样&lt;/del&gt;
感谢网友指正，因为某些通讯协定或资料格式直接规定讯息内不能放 binary data，比如最常拿来举例的就是 email 格式的 MIME，直接规定只允许 ASCII 的字元。那图片跟其它附加档案都是 binary data 怎麽办？用 base64 即可将 binary data 转成 ASCII 并附在 email 的讯息中。&lt;/p&gt;

&lt;p&gt;另外 stackoverflow 上的解釋也十分值晶參考，&lt;br&gt;
避免不同系统对不同的编码解读不同。
如用 base64 传输资料，即使两边系统解读不同，
至少可以同意「资料本身」是两边一致的，
以 Stackoverflow 上的例子来说，传两行的讯息时：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hello
world!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ASCII 的编码是长&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;72 101 108 108 111 10 119 111 114 108 100 33
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那个换行符号 10, 在不同的作业系统解读是不一样的
如果用 base64&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SGVsbG8Kd29ybGQh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一样都是 ASCII，但都是较「安全」的，也就是所有系统都一致的&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;83 71 86 115 98 71 56 75 100 50 57 121 98 71 81 104
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不过这则留言还是满好笑的 xD&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;问题留言有一位 Martin 回
We use base64 because it's more readable than Perl
笑死 xD&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="为何不干脆来个 base128, base256? 不是能变更短"&gt;为何不干脆来个 base128, base256? 不是能变更短&lt;/h2&gt;
&lt;p&gt;这问题也非常有意思，我也这样想过&lt;br&gt;
&lt;a href="https://stackoverflow.com/questions/6008047/why-is-base128-not-used" rel="nofollow" target="_blank" title=""&gt;encoding - Why is base128 not used? - Stack Overflow&lt;/a&gt;&lt;br&gt;
因为 base64 的字元其实是从 ASCII 选的，为了所有电脑都能显示，&lt;br&gt;
从上面的程式码例子也知道 decode 完预设是 ASCII 的编码。&lt;br&gt;
ASCII 裡剩下的都是控制码了，换行 &lt;code&gt;\n&lt;/code&gt; 或让电脑叫一声那种。&lt;/p&gt;

&lt;p&gt;希望有帮助到大家理解喔&lt;/p&gt;</description>
      <author>kevinluo201</author>
      <pubDate>Mon, 22 Feb 2021 18:29:52 +0800</pubDate>
      <link>https://ruby-china.org/topics/40939</link>
      <guid>https://ruby-china.org/topics/40939</guid>
    </item>
    <item>
      <title>見令如見人，理解 JWT</title>
      <description>&lt;p&gt;Dev.to 链结 &lt;a href="https://dev.to/kevinluo201/jwt-27ng" rel="nofollow" target="_blank" title=""&gt;https://dev.to/kevinluo201/jwt-27ng&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;JWT 是 &lt;a href="https://jwt.io/" rel="nofollow" target="_blank" title=""&gt;JSON Web Tokens&lt;/a&gt; 的缩写。
最近工作上需要用 JWT 来互传资讯，可能年纪渐长...觉得这东西实在使用是满简单的 xD&lt;/p&gt;

&lt;p&gt;只是结合多个概念，一开始不是很好懂&lt;/p&gt;

&lt;p&gt;纪录下理解的过程&lt;/p&gt;
&lt;h2 id="TL;DR"&gt;TL;DR&lt;/h2&gt;
&lt;p&gt;个人观点，不要打我 xD&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;可把 JWT 当成 JSON&lt;/li&gt;
&lt;li&gt;JWT 是无法修改的 JSON&lt;/li&gt;
&lt;li&gt;JWT 跟加密无关&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="Why?"&gt;Why?&lt;/h2&gt;
&lt;p&gt;先不提 JWT 怎麽实做的，先想像班上有一对情侣 Dustin 跟 Suzie...
&lt;img src="https://l.ruby-china.com/photo/kevinluo201/73f95f24-27a0-4515-8457-c986208f32a4.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;两人都是 Geek，所以在教室最后一排的 Dustin 要传纸条给第一排的 Suzie 的时候，竟然是用 JSON 的格式！&lt;/p&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"from"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Dustin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"to"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Suzie"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Do you want to have dinner with me tonight?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"place"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MacDonald's"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不过传纸条的过程中，班上就是会有人硬要打开不是给他的纸条，比如 Will 就是打开纸条并把内容改成&lt;/p&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"from"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Dustin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"to"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Erica"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;被&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Will&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;改了&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Do you want to have dinner with me tonight?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"place"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MacDonald's"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;于是不仅当晚 Dustin 只能孤零零一个人吃大麦克，Suzie 还不理他一个礼拜，悲剧。
其实一切就是他没办法控制發出去的讯息会不会被&lt;strong&gt;篡改&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;后来 Dustin 记取教训，事先跟 Suzie 约定好一个&lt;strong&gt;暗号&lt;/strong&gt; &lt;code&gt;NeverEndingStory&lt;/code&gt;  并用 HMAC SHA-256 这种不可逆的方式製作一串乱码，再把乱码附在要传的讯息后面&lt;/p&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;796e0&lt;/span&gt;&lt;span class="err"&gt;c&lt;/span&gt;&lt;span class="mi"&gt;718&lt;/span&gt;&lt;span class="err"&gt;cc&lt;/span&gt;&lt;span class="mi"&gt;2768&lt;/span&gt;&lt;span class="err"&gt;edfb&lt;/span&gt;&lt;span class="mi"&gt;67&lt;/span&gt;&lt;span class="err"&gt;a&lt;/span&gt;&lt;span class="mi"&gt;53&lt;/span&gt;&lt;span class="err"&gt;b&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="err"&gt;f&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="err"&gt;fed&lt;/span&gt;&lt;span class="mi"&gt;74&lt;/span&gt;&lt;span class="err"&gt;b&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="err"&gt;abbac&lt;/span&gt;&lt;span class="mi"&gt;61&lt;/span&gt;&lt;span class="err"&gt;baaa&lt;/span&gt;&lt;span class="mi"&gt;68876630&lt;/span&gt;&lt;span class="err"&gt;d&lt;/span&gt;&lt;span class="mi"&gt;9827714&lt;/span&gt;&lt;span class="err"&gt;a&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Suzie 打开纸条后即可以用&lt;code&gt;NeverEndingStory&lt;/code&gt; 以相同的方式将纸上的讯息转成乱码，再检查是否和纸上附的乱码一致。&lt;/p&gt;

&lt;p&gt;可以任意找一个线上的 HMAC SHA256 转换器来验证 &lt;a href="https://www.devglan.com/online-tools/hmac-sha256-online" rel="nofollow" target="_blank" title=""&gt;Free HMAC-SHA256 Online Generator Tool | Devglan&lt;/a&gt;
&lt;img src="https://l.ruby-china.com/photo/kevinluo201/7842c7c6-78ef-47a8-9f6b-295598420973.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;只要讯息跟算出来的乱码不合，即知道讯息已遭到修改或者不完全&lt;/strong&gt;。所以这个乱码很像一个&lt;strong&gt;签名&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;JWT 就是可以给 JSON 一个&lt;strong&gt;签名&lt;/strong&gt;，确保讯息没有修任何人动过。变得好像可以宣告一个 &lt;code&gt;const&lt;/code&gt; 的 JSON 再传送出去一样&lt;/p&gt;
&lt;h2 id="JWT组成"&gt;JWT 组成&lt;/h2&gt;
&lt;p&gt;JWT 其实是一串字串，有 3 个部分，以 &lt;code&gt;.&lt;/code&gt; 分开
&lt;img src="https://l.ruby-china.com/photo/kevinluo201/b4e33c75-ed97-44c6-9ae3-384dc706a5ce.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Header: 註明是用何种演算法製作签名的&lt;/li&gt;
&lt;li&gt;Payload: 就是实际讯息的 JSON&lt;/li&gt;
&lt;li&gt;Signature: 利用 Header 註明的演算法用 HMAC 方式製作出来的乱码，即签名&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;可以到 &lt;a href="https://jwt.io/" rel="nofollow" target="_blank" title=""&gt;JSON Web Tokens - jwt.io&lt;/a&gt;任意製作一个 JWT 
&lt;img src="https://l.ruby-china.com/photo/kevinluo201/3b1fc547-1b38-441e-b0b7-dbe83c82c5d3.png!large" title="" alt=""&gt;
&lt;img src="https://l.ruby-china.com/photo/kevinluo201/bd7b46f0-5df9-4244-8079-8f3e3a79be2d.png!large" title="" alt=""&gt;
左边即为製作出来的 JWT。&lt;/p&gt;

&lt;p&gt;我想这时已经有人握紧拳头了...&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;听你鬼扯！
这个 JWT 看起来根本就只是一串乱码！
什麽 JSON、指定的演算法跑到哪裡去了？&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;上面的确是漏说了一些细节 😂
Header, Payload 其实是会先经由 base64 去编码
Base64 就是个编辑的方式，可以先简单理解成一个可编码及还原的方法。
如果真的很想知道 base64 是什麽，可参考另一篇文章 &lt;a href="https://ruby-china.org/topics/40939" title=""&gt;base64 介绍&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;所以红色部色就是 header，紫色部分就是 JSON
这边用 ruby 来做看看&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'&lt;/span&gt;
&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'eyJuYW1lIjoiS2V2aW4ifQ'&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'base64'&lt;/span&gt;
&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="no"&gt;Base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# {"alg":"HS256","typ":"JWT"}&lt;/span&gt;
&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="no"&gt;Base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# {"name":"Kevin"}&lt;/span&gt;

&lt;span class="c1"&gt;# verify the signature&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'openssl'&lt;/span&gt;
&lt;span class="n"&gt;mac&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;OpenSSL&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;HMAC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"SHA256"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'mySecret'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="no"&gt;Base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlsafe_encode64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mac&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;gsub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'='&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# '=' is just padding&lt;/span&gt;
&lt;span class="c1"&gt;# "IMa4S4W1LMP1xuKVglwBagrHA5wwK9sBu-CVDKudIkg"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可能会疑惑怎麽可以直接把 payload 还原成 JSON，那 JWT 裡的资料不就大辣辣地秀出，这样不是&lt;strong&gt;不安全&lt;/strong&gt;？&lt;/p&gt;

&lt;p&gt;没错...因为 JWT 好像变成一串乱码，容易误会它很安全，其实它跟加密&lt;strong&gt;完全没有关係&lt;/strong&gt; xD &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;JWT 主要在乎资料是否被&lt;strong&gt;篡改&lt;/strong&gt;，Signature 是否一致而已&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;所以别把敏感的资讯放在裡面。&lt;/p&gt;

&lt;p&gt;我们可以先看实际使用 JWT 的程式码&lt;/p&gt;
&lt;h2 id="Demo"&gt;Demo&lt;/h2&gt;
&lt;p&gt;因为我主要用 ruby，这边利用 &lt;code&gt;ruby-jwt&lt;/code&gt; 这个 gem 来 demo。不过几乎所有的语言都有实作 jwt，可在 &lt;a href="https://jwt.io/" rel="nofollow" target="_blank" title=""&gt;JSON Web Tokens - jwt.io&lt;/a&gt; 查找相关资源。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'jwt'&lt;/span&gt;

&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="ss"&gt;first_name: &lt;/span&gt;&lt;span class="s1"&gt;'Kevin'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;last_name: &lt;/span&gt;&lt;span class="s1"&gt;'Luo'&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"my secret"&lt;/span&gt;

&lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;JWT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'HS256'&lt;/span&gt;
&lt;span class="c1"&gt;# "eyJhbGciOiJIUzI1NiJ9.eyJmaXJzdF9uYW1lIjoiS2V2aW4iLCJsYXN0X25hbWUiOiJMdW8ifQ.dZJnejsQ9cWs1hyOvCAij_Q4k87vfbQpeBIjgqYCrgs"&lt;/span&gt;

&lt;span class="n"&gt;decoded_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;JWT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;algorithm: &lt;/span&gt;&lt;span class="s1"&gt;'HS256'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;# [{"first_name"=&amp;gt;"Kevin", "last_name"=&amp;gt;"Luo"}, {"alg"=&amp;gt;"HS256"}]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此外，JWT 的格式 RFC 其实有约定一些参数可以设定，不过端看程式有没有做对应的处理，
一个常用的是「到期时间」 &lt;code&gt;exp&lt;/code&gt; ，设定一个到期时间给 JWT, 假使真的到期，decode 时即丢出&lt;code&gt;JWT::ExpiredSignature&lt;/code&gt; 这个 Exception&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'jwt'&lt;/span&gt;
&lt;span class="n"&gt;exp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt; &lt;span class="c1"&gt;# 1 hour&lt;/span&gt;
&lt;span class="n"&gt;exp_payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;exp: &lt;/span&gt;&lt;span class="n"&gt;exp&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;JWT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt; &lt;span class="n"&gt;exp_payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'HS256'&lt;/span&gt;
&lt;span class="c1"&gt;# "eyJhbGciOiJIUzI1NiJ9.eyJkYXRhIjp7ImZpcnN0X25hbWUiOiJLZXZpbiIsImxhc3RfbmFtZSI6Ikx1byJ9LCJleHAiOjE2MTM4ODU4MjF9.1_NIKXDnBVz1G6Li7_CZbcDwIk5AFaOsreK7BFDS13Q" &lt;/span&gt;

&lt;span class="k"&gt;begin&lt;/span&gt;
  &lt;span class="n"&gt;decoded_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;JWT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;algorithm: &lt;/span&gt;&lt;span class="err"&gt;‘&lt;/span&gt;&lt;span class="no"&gt;HS256&lt;/span&gt;&lt;span class="err"&gt;’&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;# [{"data"=&amp;gt;{"first_name"=&amp;gt;"Kevin", "last_name"=&amp;gt;"Luo"}, "exp"=&amp;gt;1613885821}, {"alg"=&amp;gt;"HS256"}] &lt;/span&gt;
&lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;JWT&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ExpiredSignature&lt;/span&gt;
  &lt;span class="c1"&gt;# 过期&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="应用"&gt;应用&lt;/h2&gt;
&lt;p&gt;JWT 除了可以让 2 台有共同 secret 的电脑可互传确认不会被篡改的资料外，
最常见的情境应该就是后端 API &lt;strong&gt;使用者登入&lt;/strong&gt;后發给前端的 session token 了吧
会用 API 的 Request 的 header 中裡的&lt;code&gt;Authorization Bearer [TOKEN]&lt;/code&gt;来判断来源是否可以取用该 API&lt;/p&gt;

&lt;p&gt;我觉得用 JWT 当 session token 应该有 2 个优点：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;由于 JWT 有不会被篡改特性，server 收到 token 后，可在裡面直接取用资料，比如说 user_id 之类的。所以才说&lt;strong&gt;见令如见人&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;不同使用者 JWT  因为 user_id 不同，必长得不同，而不用是检查碰撞。如果 token 是随机产生，我们还得去检查是否有碰撞 (就是 2 个使用者运气好，用到相同的乱数字串)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;目前就只用过这 2 种应用，不知道还有什麽特别的用途囉？&lt;/p&gt;

&lt;p&gt;JWT 的分享到此囉 : )&lt;/p&gt;</description>
      <author>kevinluo201</author>
      <pubDate>Sun, 21 Feb 2021 23:34:49 +0800</pubDate>
      <link>https://ruby-china.org/topics/40933</link>
      <guid>https://ruby-china.org/topics/40933</guid>
    </item>
    <item>
      <title>Rails 4 升级至 6 心得分享 </title>
      <description>&lt;p&gt;Blog 版 &lt;a href="https://dev.to/kevinluo201/rails-4-6-2iij" rel="nofollow" target="_blank" title=""&gt;https://dev.to/kevinluo201/rails-4-6-2iij&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;分享最近把专案从 Rails 4.2 硬生生地升级到 6.0 的心得。专案是公司内部用的系统。&lt;/p&gt;
&lt;h2 id="TL;DR"&gt;TL;DR&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://edgeguides.rubyonrails.org/upgrading_ruby_on_rails.html" rel="nofollow" target="_blank" title=""&gt;Upgrading Ruby on Rails — Ruby on Rails Guides&lt;/a&gt; 
细看 &lt;code&gt;General Adivce&lt;/code&gt; 再根据要升级的版本去下面找对应的章节，即可升级&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.fastruby.io/blog/rails/upgrade/testing/how-to-upgrade-rails-without-tests.html" rel="nofollow" target="_blank" title=""&gt;How to Upgrade Rails Without a Test Suite - FastRuby.io | Rails Upgrade Service&lt;/a&gt; 
如果 test coverage 过低或者根本没有写自动化测试，可参考这篇文章的策略&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="Why?"&gt;Why?&lt;/h2&gt;
&lt;p&gt;其实呢，以一个内部系统而言，因为用的都是公司自己的员工，事实上似乎没什麽特别去升级的必要。&lt;/p&gt;

&lt;p&gt;不过主要本公司为了要过某个 ISO 认证，软体这部分有个「安全性」要考量，许多 Gem 存在安全性风险需要升级才能避免。&lt;/p&gt;

&lt;p&gt;而许多 Gem 的版本被 Rails 的版本卡住，以至于说需要将 Rails 升级。
旧版本的 Rails 也渐渐不再被维护，升级 Rails 除安全性外也可以有更好的开發体验吧！或者说，使用开源框架就是要跟着社群一起成长啊！
也避免公家机关那种：蛤你们还在用 windows 95? 的这种情形&lt;/p&gt;

&lt;p&gt;另个简单的原因是：老闆就是要升级。这理由也够充分了😅&lt;/p&gt;
&lt;h2 id="专案介绍"&gt;专案介绍&lt;/h2&gt;
&lt;p&gt;我负责的专案是公司内部用的 CRM 系统，最早约 2013 年就开始第一版开始使用了。
根据 git 的情形来看，主要开發团队至少换过 5 次甚至更多。
本公司是间纯软体公司，但「内部系统」不是正式卖给客户的产品，简单说是次等公民。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Test coverage 不佳，也没有引入 CI&lt;/li&gt;
&lt;li&gt;后来都是外包，风格迥异&lt;/li&gt;
&lt;li&gt;每个团队都有自己爱用的 gem&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="过程"&gt;过程&lt;/h2&gt;
&lt;p&gt;我们只有 2 个开發者，2020 年 4 月才入职。因为一开始就知道升级 rails  是目标之一，所以满早开始就在想如何做到了。&lt;/p&gt;

&lt;p&gt;我只有两次升级的经验，
第一次是台北某德商公司，rails 3 升到 rails 4，採用的策略是把 test coverage 补到 100%。真的很屌...连 end-to-end 的测试都全补了。不过该公司在台北 + 德国有 100 多个工程师可以补测试。&lt;/p&gt;

&lt;p&gt;第二次是帮某团队从 rails 4 升到 rails 5。因为真的不是说多熟 rails 百花撩乱的设定，我那时是採偷吃步的方式...用搬的：我直接 new 了一个 rails5 的专案，然后，假如我要搬某个 model，我在新专案直接 &lt;code&gt;rails g model&lt;/code&gt;再把旧的程式码一个个搬进去 xD&lt;/p&gt;

&lt;p&gt;多年以后，我还是对 rails 升级不熟 xD
但以上两个方式都不适用了，我们只有 2 个人，coverage 只有不到 40%；另外程式码实在太多了，根本不可能用搬的，甚至有些 js 引用的方式异常神祕，我连怎麽搬都不知道。&lt;/p&gt;

&lt;p&gt;所以这次採用的流程如下，当然中间我们还是持续开發新的功能，所以就是每个礼拜安插一些时间来处理，前前后后大概花了 5 个月吧~&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;引入 CI&lt;/li&gt;
&lt;li&gt;修復既有 test&lt;/li&gt;
&lt;li&gt;补上 test&lt;/li&gt;
&lt;li&gt;移除不用的 Gem&lt;/li&gt;
&lt;li&gt;整理 feature 清单&lt;/li&gt;
&lt;li&gt;升级&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="引入 CI"&gt;引入 CI&lt;/h2&gt;
&lt;p&gt;&lt;del&gt;就是懒的在本地跑 rails test&lt;/del&gt; CI 可是确保 branch 上的 test 全过的检查机制，没过绝不要 merge 到主线裡啊！别让情况变的更糟&lt;/p&gt;
&lt;h2 id="修復既有 test"&gt;修復既有 test&lt;/h2&gt;
&lt;p&gt;好像没什麽需要补充的，不过花了非常多时间。&lt;/p&gt;
&lt;h2 id="补上 test"&gt;补上 test&lt;/h2&gt;
&lt;p&gt;虽然我们只有 2 个人，在短时间内把原来不到 40% 的测试要补到超过 80% 几乎有点不太可能，但还是须要做关键的 test。那哪些算是关键的 test 呢？以我们的专案来说是：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Model 的 Unit test&lt;/li&gt;
&lt;li&gt;Service Object 的 test&lt;/li&gt;
&lt;li&gt;对外开放的 API 的 test&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="移除不用的 Gem"&gt;移除不用的 Gem&lt;/h2&gt;
&lt;p&gt;每个团队有自己爱用的 gem，举例：有的人喜欢 httparty、有的人喜欢 RestClient、有的人喜欢 Faraday... 做人要有雅量，但写程式实在不用那麽大的雅量... 儘量把同样功能的 gem 改用同一个，否则一个 gem 就是一个 DSL，到时全部 gem 一起跟着 rails 大升级，又要重看每个 DSL 的使用说明，很耗时间。&lt;/p&gt;

&lt;p&gt;有些我预计要移掉的 gem，但一时之间移不掉，会先 fork 到自己的 github 修改再引用。不过除非是要移除的 gem 才这样做。如果还要持续用的话，还是得想办法 merge 回 upstream&lt;/p&gt;

&lt;p&gt;另外有些 Gem 根本就停止维护了，连 maintainer 都跑了，你丢 PR 也不理你的 xD&lt;/p&gt;

&lt;p&gt;其实这件事我到现在也尚未做完，所以才为什麽只升到 6.0 而已 😅 因为 2020 年年底 6.1 已经出了
但因为已经把安全性的更新都完成了~ 就先这样&lt;/p&gt;
&lt;h2 id="整理 feature 清单"&gt;整理 feature 清单&lt;/h2&gt;
&lt;p&gt;因为整体 test coverage 最后其实还是不高，这样子就要硬上风险太大了。幸好到 2021 年时我们已经接独这个系统 8 个多月了，对于这个系统的功能，已大致了解完全。
大部分缺的是 controller 或 view 的 test，Rails 裡的确要写这两种 test 不是非常好写，&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;controller test：有些没设计好的 action 的 test 很难写，要准备大量资料。不好好准备资料的话，有时候甚至也是从头 mocking 到尾。&lt;/li&gt;
&lt;li&gt;view test：无法对 partial 做 unit test，那就变成跟 controller 一样要准备好资料。不过 6.1 有  &lt;a href="https://viewcomponent.org/" rel="nofollow" target="_blank" title=""&gt;view_component&lt;/a&gt; 未来应该可以增加 partial 的 unit test&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;不过，为了达到我们的需求，我改列了一个每个页面上所有功能的清单，来做手动测试。
其实没什麽祕诀，就是做一个非常长的 checking list，依序检查
我记在 Notion 上方便共同编辑，我刚大概滑了一下，大概滑了 20 秒才滑完，所以真的很长啊！&lt;/p&gt;

&lt;p&gt;2 人大概花了 2~3 天才完成这个清单。
而且完整地测一次大概要 1 天，所以测试很珍贵...
我们最后是测了 2 次，最后一次是在上线前。
真的满累的，真心觉得 controller, view 的 auto test 不要省啊。
就算有 QA 也别累死 QA, 让 QA 去找更有价值的 edge case 吧！&lt;/p&gt;

&lt;p&gt;后来 ruby weekly 上有分享这篇文章
&lt;a href="https://www.fastruby.io/blog/rails/upgrade/testing/how-to-upgrade-rails-without-tests.html" rel="nofollow" target="_blank" title=""&gt;How to Upgrade Rails Without a Test Suite - FastRuby.io | Rails Upgrade Service&lt;/a&gt;
主要核心也是列出完整的 feature 让开發者或 QA 在上 production 前手动测试，看来逻辑还是正确囉&lt;/p&gt;
&lt;h2 id="升级"&gt;升级&lt;/h2&gt;
&lt;p&gt;前面都是在准备升级，现在才真的开始要升
升级时，直接切一个 branch 就开始做啦！
基本上是要参照前人的心得做： &lt;a href="https://edgeguides.rubyonrails.org/upgrading_ruby_on_rails.html" rel="nofollow" target="_blank" title=""&gt;Upgrading Ruby on Rails — Ruby on Rails Guides&lt;/a&gt;
&lt;strong&gt;它的第一章 General Advice 非常重要，请务必详读&lt;/strong&gt;
基本上 Rails guide 裡把跳下个版本会遇到的事情都写出来了，就不再赘述。
分享一些特别的调整：&lt;/p&gt;
&lt;h3 id="4.2 -&gt; 5.0"&gt;4.2 -&amp;gt; 5.0&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Controller test 的 params 改为 keyword 形式
&lt;code&gt;
Deprecated style: get “/new”, { id: 1 }, { “X-Extra-Header” =&amp;gt; “123” }
New keyword style: get “/new”, params: { id: 1 }, headers: { “X-Extra-Header” =&amp;gt; “123” }
&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;environment config 要明写 active_job 的 queue adapter
因为 4 还没有用到 activeJob，我们都是直接 &lt;code&gt;include Sidekiq::Worker&lt;/code&gt;
5 以后 action mailer 直接继承 active job
&lt;code&gt;ruby
config.active_job.queue_adapter     = :sidekiq
&lt;/code&gt;
### 5.0 -&amp;gt; 5.1&lt;/li&gt;
&lt;li&gt;Dirty 的方法名称变了
&lt;code&gt;ruby
:changed 变成要选要 :will_change 或 :saved_changes
&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="5.1 -&gt; 5.2"&gt;5.1 -&amp;gt; 5.2&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;belongs_to 变成 required，如果要保持空值需特别加註 required: false。可以先关掉设定
&lt;code&gt;config.active_record.belongs_to_required_by_default = true&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="5.2 -&gt; 6.0"&gt;5.2 -&amp;gt; 6.0&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;autoloading 改成用 Zeitwerk。可以设定用原来的方式。
&lt;code&gt;config.autoloader = :classic&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="如果能重来...我会"&gt;如果能重来...我会&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;绝对先把要移除的 gem 来移除掉，&lt;/li&gt;
&lt;li&gt;如果还是要持续使用某些不支援新版本 rails 的 gem，先 fork 一份，但确保能 merge 回 upstream，否则对后来的同事来说，依然是一个没人维护的 gem。否则就是要自告奋勇去当 maintainer 了&lt;/li&gt;
&lt;li&gt;补齐  test，Rails 有了 &lt;a href="https://viewcomponent.org/" rel="nofollow" target="_blank" title=""&gt;view_component&lt;/a&gt;的概念，我想 controller 的 html 回应应该比较好做 test，补好 controller 应该破 80% 的 test coverage 没什麽问题，也可以避免更多问题了&lt;/li&gt;
&lt;li&gt;觉得无论如何都需要一份 Feature 清单，可以是列表、Workflow 或 User Story Mapping 的形式。如果有资深的 QA，应该会本来就会做一份&lt;/li&gt;
&lt;li&gt;最后上线后，出问题的地方是 cronjob，上面讲的方法的确没特别关心 cronjob。cronjob 裡呼叫 async 的方法，如 deliver_later，因为没设定 &lt;code&gt;config.active_job.queue_adapter     = :sidekiq&lt;/code&gt;，信根本不会寄出去&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;大概就是这样，倒不是什麽教学，祝大家升级圆满&lt;/p&gt;</description>
      <author>kevinluo201</author>
      <pubDate>Tue, 16 Feb 2021 12:36:02 +0800</pubDate>
      <link>https://ruby-china.org/topics/40916</link>
      <guid>https://ruby-china.org/topics/40916</guid>
    </item>
    <item>
      <title>[上海] 上海合衣盒创意设计有限公司招聘 Rails 工程师 1 名</title>
      <description>&lt;h3 id="公司介绍"&gt;公司介绍&lt;/h3&gt;
&lt;p&gt;CHUANBOX 想穿 成立于 2019 年初，专注于人工智能身形匹配推荐的女装订阅平台，通过 AI 人工智能数据分析，以及 1v1 身形顾问的专业搭配，为女人推荐最合适自己身形的服装。&lt;/p&gt;

&lt;p&gt;我们想让每个身材的女人都值得拥有「穿衣购衣的自信和乐趣」，不再因为身材局限而有任何穿衣搭配的困扰！也希望这全新形态的购衣模式，能够为女性带来更便利、更贴心，以及更有质感的生活体验。&lt;/p&gt;

&lt;p&gt;公司团队目前 20 人，已经完成了订阅推荐服务的商业模式的基本搭建，在半年内，用户增长数呈 2 倍数增长，现阶段用户活跃度超过 80%。在面对订阅模式兴起，CHUANBOX 想穿以用户最在意身形的这项需求为出发点：找到匹配身形的服装，结合数据、科技和新型商业模式，快速而精准的提供用户真正需要的服务。&lt;/p&gt;

&lt;p&gt;CHUANBOX 想穿是一个充满创意、活力的年轻团队，工作环境雅致而舒适，欢迎你的加入！与我们共同为女性打造一个全新的购物体验，把这样的美好生活风格散播给更多人！
企业文化为美式作风，扁平、自由，团队很年轻、弹性、国际化、有活力，希望想法多元、热情；希望对时装、电商、科技有热情的伙伴加入，尤其对时尚经营有想法的人，很欢迎来这里一起实现！&lt;/p&gt;
&lt;h3 id="形象照片"&gt;形象照片&lt;/h3&gt;
&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2019/7736844c-be3a-49b3-a64c-6ab0d1da94d9.jpg!large" title="" alt=""&gt;
&lt;img src="https://l.ruby-china.com/photo/2019/e1634511-2c28-40f4-a3b2-6e34b1870e8f.jpg!large" title="" alt=""&gt;
 &lt;img src="https://l.ruby-china.com/photo/2019/8ae0e847-a7bd-4d43-bd41-af727ed2fe8c.jpg!large" title="" alt=""&gt;&lt;/p&gt;
&lt;h3 id="产品信息"&gt;产品信息&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;产品名称：CHUANBOX 想穿&lt;/li&gt;
&lt;li&gt;Slogan：看穿你身形，想穿就穿&lt;/li&gt;
&lt;li&gt;亮点和特色：
 「CHUANBOX 想穿」通过人工智能匹配系统的身形数据分析，以及 1v1 身形顾问的专业穿搭技巧，为五大身型（梨形/苹果形/报纸形/漏斗形/草莓形）的女性找到适合的美衣。
致力于帮助用户了解身形，强化身材优点并真正解决女性因为身材局限带来的穿衣困扰。并且提供”在家试穿”以及”先试后买”的服务特色，无需出门逛街，即可定期满足购衣的需求。&lt;/li&gt;
&lt;li&gt;产品官网：&lt;a href="https://www.chuanbox.com/" rel="nofollow" target="_blank"&gt;https://www.chuanbox.com/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ok...这儿开始是工作职位要求&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="能力要求"&gt;能力要求&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;熟 Rails 架构&lt;/li&gt;
&lt;li&gt;有 TDD/BDD 经验，会在 Rails 环境下做各类自动化测试&lt;/li&gt;
&lt;li&gt;熟 Database，了解 MySQL 架构，能够优化 SQL &lt;/li&gt;
&lt;li&gt;熟悉 Rails 及其它 web service 的部署 &lt;/li&gt;
&lt;li&gt;了解 Bootstrap&lt;/li&gt;
&lt;li&gt;喜欢创业氛围，有上进心与自我驱动力&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="以下为加分"&gt;以下为加分&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;熟 Docker&lt;/li&gt;
&lt;li&gt;有撰写 Jenkins 脚本经验&lt;/li&gt;
&lt;li&gt;能用 Vue.js 开发前端&lt;/li&gt;
&lt;li&gt;有敏捷开发经验，操作过 extreme programming 尤佳&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="工作内容"&gt;工作内容&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;开发公司目前 Rails, Webpack(yes, 前后端) 等电商性质产品：网站、库存管理等&lt;/li&gt;
&lt;li&gt;撰写 API 规格&lt;/li&gt;
&lt;li&gt;负责云端伺服器运维&lt;/li&gt;
&lt;li&gt;负责撰写各类测试或者支援测试&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="工作地点"&gt;工作地点&lt;/h3&gt;
&lt;p&gt;上海市徐汇区 (1 号线衡山路 - 常熟路之间)&lt;/p&gt;
&lt;h3 id="工作时间"&gt;工作时间&lt;/h3&gt;
&lt;p&gt;9-12 點，12:00-13:00 午休，13:00-18:00&lt;/p&gt;
&lt;h3 id="薪资范围"&gt;薪资范围&lt;/h3&gt;
&lt;p&gt;15K-20K(可依學經歷、個人能力調整)&lt;/p&gt;
&lt;h3 id="需求人数"&gt;需求人数&lt;/h3&gt;
&lt;p&gt;1-2 位&lt;/p&gt;
&lt;h3 id="联络方式"&gt;联络方式&lt;/h3&gt;
&lt;p&gt;请将履历寄到电子信箱：罗先生 kevin.luo@chuanbox.com
        主旨请注明 RubyChina 喔 : )&lt;/p&gt;</description>
      <author>kevinluo201</author>
      <pubDate>Tue, 30 Jul 2019 23:53:22 +0800</pubDate>
      <link>https://ruby-china.org/topics/38888</link>
      <guid>https://ruby-china.org/topics/38888</guid>
    </item>
  </channel>
</rss>
