---
# System prepended metadata

title: 20220822 RSPEC
tags: [Rails, Duck typing, Ruby]

---

# 20220822 RSPEC
###### tags: `Ruby` `Rails` `Duck typing`

## 測試
> hello 尼好嗎
> HIHI~ 晚上好啊～ 
>  偷偷聊天 抓！

```shell=
$ rails _6.1.6_ new <name>
$ bundle add rspec-rails

$ rails g rspec:install

$ rails g model Product title description:text price:integer

$ rails g rspec:model Cart #可以產生出Cart的model的測試檔
```
![](https://i.imgur.com/lE1tYny.png)
會提示你一些東西的奇妙指令
```shell!
$ rails g 
```
![](https://i.imgur.com/1YsoDtR.png =200x)
這只是一部份！

如果出現這個：
![](https://i.imgur.com/gEvRMsB.png)
/Gemfile
![](https://i.imgur.com/YcHwuyr.png)
可以去參考柚子的部落格喔！
https://hackmd.io/@yoji/HJDIzuQRq


## 小設定（非必要）
把
```ruby=
config.default_formatter = "doc"
```
加到 spec_helper.rb 的 RSpec.configure 的 block 裡，每次執行 rspec 後得到的測試訊息會變成這樣：
```ruby
CartItem
  每個 Cart Item 都可以計算它自己的金額（小計）。

Cart
  可以把商品丟到到購物車裡，然後購物車裡就有東西了。
  如果加了相同種類的商品到購物車裡，購買項目（CartItem）並不會增加，但商品的數量會改變。
        (略)

Finished in 0.02977 seconds (files took 0.84509 seconds to load)
8 examples, 0 failures
```

## 購物車測試
[龍哥原始教材](https://railsbook.tw/chapters/27-shopping-cart-part-1)

購物車存取紀錄：
- cookie 容量較小 通常擺認證
- localstorage 

> https://pragprog.com/titles/rails6/agile-web-development-with-rails-6/


```shell
rails g rspec:install
```

對於購物車的期望：

* 可以把商品丟到到購物車裡，然後購物車裡就有東西了。
* 如果加了相同種類的商品到購物車裡，購買項目（CartItem）並不會增加，但商品的數量會改變。
* 商品可以放到購物車裡，也可以再拿出來。
* 每個 Cart Item 都可以計算它自己的金額（小計）。
* 可以計算整台購物車的總消費金額。
* 特別活動可搭配折扣（例如聖誕節的時候全面打 9 折，或是滿額滿千送百或滿額免運費）。

### 測試開始
```ruby!
require 'rails_helper'

Cart = 1

RSpec.describe Cart, type: :model do
  it "works" do 
    expect(1).to be 1 
  end 
end
```
這樣寫是強迫讓他測試成功，但不會這樣寫。
也不會用`rails g model Cart`，因為沒有要存進資料庫。

所以可以去 app/model 手動自己新增檔案
![](https://i.imgur.com/QnGtDU0.png)

這樣測試也會過：
![](https://i.imgur.com/PJmfHGE.png)

### duck typing
> 為什麼 ruby 要寫測試
> 沒有型別檢查，所以要測 

```ruby
#duck typing
def aaa(x)
    x.abc
end
```
我不在意你的型別（整數或陣列），只要能支援abc方法就好了。
這樣會把`x`稱為`鴨子型別(duck typing)`


這時候x沒有abc方法就爆炸了，問題會在運行時才會發現。
雖然這樣的寫法是方便的，但是相對來說是鬆散的。
如果有錯，上線後才會被發現，這時候就已經來不及了。
所以寫測試的目的是因為沒辦法保證寫的東西一定是對的，因為沒有型別的檢查。
所以寫測試在 Ruby 圈是很重要的。

TypeScripts 是javascript的superset
Java

### initialize
![](https://i.imgur.com/YK9qkUI.png)
執行 add ，都會產生一個新陣列，這樣他的商品永遠就只會有一樣
所以可以用`initialize`
```ruby!
def initialize
  @items = []
end
```
new 之後才會執行，所以可以不用加到 add 裡去做。

```ruby
list = [1,2,3,4,5]

target = 6

p list.find{ |a| a == target }
```


![](https://i.imgur.com/4iFBhhU.png)
因為沒有做判斷，所以才會有8個
但是以目前陣列的資料結構無法滿足條件。
所以可以用hash。

雖然可以直接用hash，但之後還會有其他功能。
所以另外再開新的資料夾
![](https://i.imgur.com/KNfRZnC.png)
 
![](https://i.imgur.com/WUkazgZ.png)

### 這邊超複雜的～
**`initialize(product_id, quantity = 1)`**
增加彈性

```ruby!
#/cart.rb
def add(product_id, quantity = 1 )
  found_item = @items.find{ |item| 
    item.product_id == product_id}

  if found_item
    @items += 1
    found_item.increment(quantity)
  else
    @items << CartItem.new(product_id, quantity)
  end 
end

#/cart_item.rb
class CartItem
  attr_reader :product_id, :quantity

  def initialize(product_id, quantity = 1)
    @product_id = product_id
    @quantity = quantity 
  end
end
```
如果沒有講數量，可以藉由參數的預設值去 +1
不指定 quantity > 初始值 為 1
有指定 quantity > 可一次增加 > 1 的數量

```ruby=
cart.add(apple)     # +1 apple
cart.add(banana, 2) # +2 banana
```

### 第三項
```ruby!
cart.items.fitst <-- 裡面是CartItme
```
ruby 裡充滿了省略過後的產物:
```ruby!
# before
expect(cart.items.count).to(be(2))
# after
expect(cart.items.count).to be 2
```
#### 一心二用？！
**一個 `it` 裡面盡量只有 一個 `expect`**
頂多兩個

這邊只想知道拿出來的是否為商品
``` ruby!
expect(cart.items.first.product).to be_a Product
```
但也可以更精準
``` ruby!
expect(cart.items.first.product.price).to be 10
```
### factory bot
> 快速產生資料

`$ bundle add factory_bot_rails`

![](https://i.imgur.com/9rok7kx.png)

```ruby!
p1 = FactoryBot.create(:product)
```
這邊這樣寫直接壞給你看
![](https://i.imgur.com/Ckc5tK5.png)

還記得我們價錢是寫多少嗎？
是 **10** 
所以當然會壞，可以這樣寫：
```ruby!
p1 = FactoryBot.create(:product, price: 10)
```
**在業界也是在用工廠來生產資料，這個一定要學會**

可以省略**FactoryBot**
在/spec/rails_helper.rb放這個
```ruby!
RSpec.configure do |config|
  config.include FactoryBot::Syntax::Methods
end
```
```ruby!
p1 = create(:product, price: 10)
```

```ruby!
@items.reduce(0) {|acc, cv|
  acc + cv
}
# 0 是初始值
```

class Product
end

PORO = Plain Old Ruby Object
沒有繼承的class

### 時間套件（測試用 控制時間，停在某時期）
[timecop](https://github.com/travisjeffery/timecop)
```$bundle add timecop```
Time.now.year 現在的時間
```ruby!
Timecop.travel(t) 
expect(cart.total_price).to be 117
# 測完記得跳回正常的時間
Timecop.return
```

### 滿千送百
![](https://i.imgur.com/hzagYMm.png)
現判斷是否為聖誕節，
如果是就也有滿千送百；
如不是聖誕節，
就只有滿千送百。
這邊的順序要注意不能顛倒，不然結果會不一樣。

### 進階功能
因為購物車將會以 Session 方式儲存，所以：

1. 可以將購物車內容轉換成 Hash 並存到 Session 裡。
2. 也可以存放在 Session 的內容（Hash 格式），還原成購物車的內容。

為甚麼要先把購物車內容換成 Hash ？
> 這是因為沒有經過這樣的轉換，存進去的就會是一段記憶體位址，或是可以說是物件，當你換頁的時候，這段就會消失。
> ![](https://i.imgur.com/agx48ar.png =400x)

#### be vs eq
```shell!
1 be 1 O
1 eq 1 O

"1" be "1" X
"1" eq "1" O
```
be 會去比較是否為同一物件
eq 指得是`==`
更詳細的介紹：
https://medium.com/lynns-dev-blog/rails-%E6%96%B0%E6%89%8B%E6%9D%91-rspec-%E4%B8%AD-eq-eql-be-%E7%9A%84%E5%B7%AE%E5%88%A5-226192f8f59e

## 序列化 serialize
剛剛 hash 轉Session這個過程叫做序列化。
[維基百科-序列化](https://zh.wikipedia.org/zh-tw/序列化)


## rubocop 檢查ruby語法套件
https://github.com/rubocop/rubocop

![](https://i.imgur.com/4277tsv.png =300x)

