CubicLouve

Spring_MTの技術ブログ

Macで自由にパケットのフィルタリング、帯域制限、パケロス率の設定をする

Macにおいて、ネットワーク帯域の制御などはNetwork Link Conditionerで対応可能です。

下記ページよりAdditional Tools for Xcode を探してdmgをダウンロードし、その中にあるNetwork Link Conditioner.prePane からインストール可能です。

https://developer.apple.com/download/all/

そうすると、システム環境設定の中にNetwork Link Conditionerがあるので、そこから利用可能です。

このNetwork Link Conditionerを利用すれば、帯域幅の制限やパケロス率の設定などを簡単に実現できます。

ただし、UDPパケットを全部落としつつ、TCPパケットについて帯域幅やパケロス率を設定したいという複雑なネットワーク環境を再現するためにはNetwork Link Conditionerだけでは実現できませんでした。

そこで、macOSにある、packet filterとdummynetを組み合わせることでこのような複雑なネットワーク環境を再現できました。

この記事ではpacket filterやdummynetの仕組みを紹介しつつ、どのように設定すれば、このような複雑なネットワーク環境を再現できるかをご紹介します。

実際 、Network Link Conditionerはpacket filterやdummynetをGUIからうまく設定できるような感じになっています。

packet filter(pf)

packet filter(pf)は、TCP/IPのフィルタリングとアドレス変換行うOpenBSDでの仕組みです。

macOSBSD系OSなので、pfを使うことでTCP/IPのフィルタリングを行うことになります。(iptablesとかではない)

pf.conf

packet filterの設定ファイルになります。

ここでパケットフィルターの設定をします。

macOSだと、/etc/pf.conf というファイルが最初からあるかと思います。

ここでは、 pf.conf において、パケットフィルタリングについての設定についてのみ言及します。

特定のIPアドレスからのUDPパケットを落とす設定例は下記のとおりです。

block return in proto udp from 192.0.2.1 to any
  • アクション(1つ目の区切り) パケットをブロックするための block というアクションを指定しています。 2つ目の指定で return としていますが、これは、tcpパケットに対して TCP RST を返し、UDP およびその他のパケットに対して ICMP UNREACHABLE を返すようにするという指定です。 単純に落とすだけであれば、drop を指定してください。

  • in以降のオプション in ( または out ) を指定することで、 受信パケット(送信パケット)を対象にするかを指定します。

proto で対象となるprotocolを指定します。今回は udp を指定しています。

from と toでそれぞれ、送信元アドレスやポート 宛先アドレスやポートを指定します。

他でできることは、 man pf.conf で確認してもらえると。

他にもアクションは様々設定できます。

pfctlコマンドとオプション(抜粋)

pfctlコマンドを利用して、packet filterの状況やpf.confの反映を行います。

確認オプション

-s のあとに続く文字列で取得できるもの選べます。

-v をつけることでより詳細な情報をみれるのと、-v を複数回書くことで詳細度が上がります。

気をつけたいのは、 -vvv とかではなく、 -v -v などと複数回書く必要があることです。

-si or -s info

filterの統計情報などが見れます

-sr or -s rules

現在ロードされているルールを表示します。

反映

-f でルールの反映を行います。 ファイル or 標準出力でも可能。

n をつけることで実際には反映させずparseだけ行うようにできます。

dummynetとdnctlコマンド

dummynetは帯域のコントロール、ネットワークトラッフィクの遅延やパケロス率の制御などを行えるネットワークのエミュレーターです。

いわるゆtraffic shaperですね。

BSD系のOSに組み込まれており、macOSにも標準でインストールされています。

このdummynetはdnctlコマンドで制御を行います。

dnctlコマンドではpipeを使って帯域幅、遅延、キューサイズ、パケロス率を設定できます。

コマンド例

dnctl pipe 1 config delay 250ms bw 1Mbit/s plr 0.1

ここでは、遅延250 ms 、帯域幅 1M bit/s、パケロス率10%を pipe 1に設定するコマンドになっています。

dnctlだけではこの設定はつかわれません。

packet filterを使ってdummynetの設定にトラフィックを流すようにします。

この設定については、dnctlのmanにしかなく、pf.confのmanには記載されていないので要注意です。

pf.confには下記のように設定します

dummynet in quick proto tcp all pipe 1

この場合tcpのパケットがpipe 1のdummynetを通ることになります。

dnctlで設定した内容は

dnctl pipe show

こんな感じで見れます。

全部合わせてみると

UDPパケットを全部落として、TCPパケットについてはパケロス10%遅延250ms 帯域 1M bit /s にしてみます。

dnctlで下記を設定

dnctl pipe 1 config delay 250ms bw 1Mbit/s plr 0.1

pf.confには下記を設定

block return in proto udp from 192.0.2.1 to any
dummynet out quick proto tcp all pipe 1

参考

OpenBSD PF: User's Guide

PF: OpenBSD パケットフィルター

トラフィックシェーピング - Wikipedia

パケットフィルタ規則の構文 - Oracle® Solaris 11.3 でのネットワークのセキュリティー保護

http://info.iet.unipi.it/~luigi/dummynet/

https://www.jstage.jst.go.jp/article/itej/64/10/64_1473/_pdf

Bandwidth Throttling on macOS - Lei's Blog

qiita.com

iptabコマンドとipcountコマンド

macOSでiptableってあるのかなと思ってコマンドを呼び出そうとしたら、iptabコマンドとipcountコマンドを偶然見つけた。

iptabコマンド

iptabコマンドはIPv4形式での各ネットマスクに応じたアドレス数やプレフィックスの一覧を表示をする

% iptab
+----------------------------------------------+
| addrs   bits   pref   class  mask            |
+----------------------------------------------+
|     1      0    /32          255.255.255.255 |
|     2      1    /31          255.255.255.254 |
|     4      2    /30          255.255.255.252 |
|     8      3    /29          255.255.255.248 |
|    16      4    /28          255.255.255.240 |
|    32      5    /27          255.255.255.224 |
|    64      6    /26          255.255.255.192 |
|   128      7    /25          255.255.255.128 |
|   256      8    /24      1C  255.255.255.0   |
|   512      9    /23      2C  255.255.254.0   |
|    1K     10    /22      4C  255.255.252.0   |
|    2K     11    /21      8C  255.255.248.0   |
|    4K     12    /20     16C  255.255.240.0   |
|    8K     13    /19     32C  255.255.224.0   |
|   16K     14    /18     64C  255.255.192.0   |
|   32K     15    /17    128C  255.255.128.0   |
|   64K     16    /16      1B  255.255.0.0     |
|  128K     17    /15      2B  255.254.0.0     |
|  256K     18    /14      4B  255.252.0.0     |
|  512K     19    /13      8B  255.248.0.0     |
|    1M     20    /12     16B  255.240.0.0     |
|    2M     21    /11     32B  255.224.0.0     |
|    4M     22    /10     64B  255.192.0.0     |
|    8M     23     /9    128B  255.128.0.0     |
|   16M     24     /8      1A  255.0.0.0       |
|   32M     25     /7      2A  254.0.0.0       |
|   64M     26     /6      4A  252.0.0.0       |
|  128M     27     /5      8A  248.0.0.0       |
|  256M     28     /4     16A  240.0.0.0       |
|  512M     29     /3     32A  224.0.0.0       |
| 1024M     30     /2     64A  192.0.0.0       |
| 2048M     31     /1    128A  128.0.0.0       |
| 4096M     32     /0    256A  0.0.0.0         |
+----------------------------------------------+

ipcountコマンド

ipcountコマンドはIPアドレスの範囲の計算をしてくれるコマンドです

例を上げると

IPアドレス範囲の表示

% ipcount 192.0.2.0/24
  192.0.2/24   192.0.2.0 - 192.0.2.255     [256]

IPアドレス範囲の計算

% ipcount 192.0.2.0 - 192.0.2.127
  192.0.2.0/25  192.0.2.0 - 192.0.2.127     [128]

中途半端な範囲でも計算してくる

% ipcount 192.0.2.0 - 192.0.2.100
    192.0.2.0/26        192.0.2.0 - 192.0.2.63      [64]
   192.0.2.64/27      192.0.2.64 - 192.0.2.95      [32]
   192.0.2.96/30      192.0.2.96 - 192.0.2.99      [4]
   192.0.2.100/32     192.0.2.100 - 192.0.2.100     [1]

rubyのIPAddrクラスを使えばIPアドレスがCIDR範囲に含まれるかもチェックできる

rubyにはIP アドレスを扱うのためのIPAddrクラスがあります。

docs.ruby-lang.org

このIPAddrクラスを使えば、IPアドレスがCIDR範囲に含まれるかもチェックできます。

irb(main):001:0> require 'ipaddr'
=> true
irb(main):002:0> cidr_sample = IPAddr.new("192.0.2.0/24")
=> #<IPAddr: IPv4:192.0.2.0/255.255.255.0>
irb(main):003:0> cidr_sample.include? "192.0.2.1"
=> true
irb(main):004:0> cidr_sample.include? "192.0.2.255"
=> true
irb(main):005:0> cidr_sample.include? "192.1.2.0"
=> false

便利!

参考

datatracker.ietf.org

MTUについて

MTUとは

MTUは最大転送単位(Maximum Transmission Unit)

MTUはデータリンク層(L2)の性質となる。

RFC 791: Internet Protocol

データリンク層での最大のフレーム長(データリンク層のパケットを表すときにはフレームが使われる。)

MTUはデータリンク層での性質なので、IP層(L3)のヘッダーまでを含んでいる。

IPはデータリンクの上位層なので、MTUを隠蔽する役割を持つ。

そこで、データリンクを司るルーターなどはMTUに応じてIPデータグラムを分割する(IP fragmentation フラグメンテーション)。 (データグラムはIPなどのネットワーク層以上でのパケット単位のデータ構造を持つプロトコルで利用される表現)

IPヘッダーには分割されたパケットを管理するためのフラグがある。

https://packetpushers.net/wp-content/uploads/2019/11/IPv4-Headers-Standard-Fragmentation-Highlighted.png

分割されたIPデータグラムを元のIPデータグラムに戻す再構築の処理は終点の宛先ホストでのみ行われる。

TCPの場合

トランスポート層(L4)でTCPを使う場合、一度にTCPで含められるアプリケーションデータのサイズは、MTUからIPヘッダー(20バイト)とTCPヘッダー(20バイトとオプションがあると60バイトまで増える)を除いたバイト数となる。

このサイズをMSS(Mazimum Segment Size 最大セグメント長)と呼ぶ。(セグメントはTCPに含まれるデータの表すときに使う)

TCPでは3ウェイ・ハンドシェイクのときに、コネクション確立要求を送るときに、送信側、受信側ともに自身のMSSを通知し、小さいMSSを採用する。

MSSに沿って、データを区切って送信されるため、IPでの分割は行わず、TCP側で再構築をする。

再送処理はMSS単位で行われる。

手元でWireSharkで確認してみた例

MSSを相互に通知している様子

MSSによって分割されたデータを再構築している

参考文献

インターネット用語1分解説~MTUとは~ - JPNIC

https://www.cloudflare.com/ja-jp/learning/network-layer/what-is-mtu/

【図解】MTUとMSS, パケット分割の考え方 ~IPフラグメンテーションとTCPセグメンテーション~ | SEの道標

MTUの最適値を調べる方法 | server-memo.net

MySQLにおける外部キー作成時の自動インデックス生成

MySQLの外部キー制約において、外部キーと参照キーにはインデックスが必要です。

下記はMySQL 8.0のドキュメントですが、5.7でも同じような内容となっています。

dev.mysql.com

参照元のテーブルには、外部キーのカラムが同じ順序で最初のカラムとしてならぶインデックスが必要です。 もし該当するindexがなければ、MySQLは自動でインデックスを作成します。 この自動で作られたインデックスは後で追加したインデックスが外部キーのインデックス要件を満たす場合、暗黙的に削除されることがあります。

これを実際に試してみようと思います

確認したMySQLのバージョンは 8.0.29 です。

まずテーブルを作ってみます

mysql> CREATE TABLE `users` (
    ->   `id` int NOT NULL,
    ->   PRIMARY KEY (`id`)
    -> ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Query OK, 0 rows affected (0.01 sec)

mysql> CREATE TABLE `user_foo` (
    ->   `id` int NOT NULL AUTO_INCREMENT,
    ->   `user_id` int NOT NULL,
    ->   `foo` int  NOT NULL,
    ->   PRIMARY KEY (`id`),
    ->   CONSTRAINT FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
    -> ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Query OK, 0 rows affected (0.01 sec)

これでテーブル定義を見てみます。

mysql> SHOW CREATE TABLE user_foo;
+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table    | Create Table                                                                                                                                                                                                                                                                                                         |
+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| user_foo | CREATE TABLE `user_foo` (
  `id` int NOT NULL AUTO_INCREMENT,
  `user_id` int NOT NULL,
  `foo` int NOT NULL,
  PRIMARY KEY (`id`),
  KEY `user_id` (`user_id`), <----- CREATE文にないindexが追加されている
  CONSTRAINT `user_foo_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci |
+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

CREATE文にないインデックスが自動で作られます。

外部キーのcolumnが同じ順序で最初のカラムとしてならぶインデックスが必要です。 これを確認してみましょう。

外部キーを含む複数カラムに対してセカンダリインデックスを追加してみます。

mysql> CREATE TABLE `users` (
    ->   `id` int NOT NULL,
    ->   PRIMARY KEY (`id`)
    -> ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Query OK, 0 rows affected (0.01 sec)

mysql> CREATE TABLE `user_foo` (
    ->   `id` int NOT NULL AUTO_INCREMENT,
    ->   `user_id` int NOT NULL,
    ->   `foo` int  NOT NULL,
    ->   PRIMARY KEY (`id`),
    ->   KEY (`user_id`, `foo`),
    ->   CONSTRAINT FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
    -> ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Query OK, 0 rows affected (0.01 sec)

mysql> SHOW CREATE TABLE user_foo;                                                                                                                                                                                     +----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table    | Create Table                                                                                                                                                                                                                                                                                                               |
+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| user_foo | CREATE TABLE `user_foo` (
  `id` int NOT NULL AUTO_INCREMENT,
  `user_id` int NOT NULL,
  `foo` int NOT NULL,
  PRIMARY KEY (`id`),
  KEY `user_id` (`user_id`,`foo`),
  CONSTRAINT `user_foo_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci |
+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

今度は自動でのインデックスが追加されていません。

user_idとfooのセカンダリインデックスで外部キー制約のインデックスの要件を満たしたためですね。

では今度はuser_idとfooを逆にしてみます。

mysql> CREATE TABLE `users` (
    ->   `id` int NOT NULL,
    ->   PRIMARY KEY (`id`)
    -> ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Query OK, 0 rows affected (0.01 sec)

mysql> CREATE TABLE `user_foo` (
    ->   `id` int NOT NULL AUTO_INCREMENT,
    ->   `user_id` int NOT NULL,
    ->   `foo` int  NOT NULL,
    ->   PRIMARY KEY (`id`),
    ->   KEY (`foo`, `user_id`),
    ->   CONSTRAINT FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
    -> ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Query OK, 0 rows affected (0.01 sec)

mysql> SHOW CREATE TABLE user_foo;
+----------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table    | Create Table                                                                                                                                                                                                                                                                                                                                        |
+----------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| user_foo | CREATE TABLE `user_foo` (
  `id` int NOT NULL AUTO_INCREMENT,
  `user_id` int NOT NULL,
  `foo` int NOT NULL,
  PRIMARY KEY (`id`),
  KEY `foo` (`foo`,`user_id`),
  KEY `user_id` (`user_id`),
  CONSTRAINT `user_foo_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci |
+----------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

今度は 外部キーのcolumnが同じ順序で最初のカラムとしてならぶインデックスにならなかったため、user_id単独のセカンダリインデックスが自動で追加されました。

今度は外部キー制約を満たすために追加したインデックスが不要になるインデックスが追加されたときの挙動を見てみます。

mysql> CREATE TABLE `users` (
    ->   `id` int NOT NULL,
    ->   PRIMARY KEY (`id`)
    -> ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Query OK, 0 rows affected (0.01 sec)

mysql> CREATE TABLE `user_foo` (
    ->   `id` int NOT NULL AUTO_INCREMENT,
    ->   `user_id` int NOT NULL,
    ->   `foo` int  NOT NULL,
    ->   PRIMARY KEY (`id`),
    ->   CONSTRAINT FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
    -> ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Query OK, 0 rows affected (0.01 sec)

mysql> SHOW CREATE TABLE user_foo;
+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table    | Create Table                                                                                                                                                                                                                                                                                                         |
+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| user_foo | CREATE TABLE `user_foo` (
  `id` int NOT NULL AUTO_INCREMENT,
  `user_id` int NOT NULL,
  `foo` int NOT NULL,
  PRIMARY KEY (`id`),
  KEY `user_id` (`user_id`), <---- 自動で追加されたインデックス
  CONSTRAINT `user_foo_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci |
+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

自動でインデックスが追加されています。

では、新たにインデックスを追加してみます。

mysql> ALTER TABLE user_foo ADD INDEX (`user_id`,`foo`); 
Query OK, 0 rows affected (0.02 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> SHOW CREATE TABLE user_foo;
+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table    | Create Table                                                                                                                                                                                                                                                                                                               |
+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| user_foo | CREATE TABLE `user_foo` (
  `id` int NOT NULL AUTO_INCREMENT,
  `user_id` int NOT NULL,
  `foo` int NOT NULL,
  PRIMARY KEY (`id`),
  KEY `user_id` (`user_id`,`foo`),
  CONSTRAINT `user_foo_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci |
+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

user_id単体ではられていたインデックスが削除され、ALTERによって追加されたインデックスのみが残りました。

ではこのインデックスを削除してみるとどうなでしょう。

mysql> ALTER TABLE user_foo DROP INDEX `user_id`;
ERROR 1553 (HY000): Cannot drop index 'user_id': needed in a foreign key constraint

この場合は、自動でインデックスが追加されるわけではなく、外部キー制約に必要なるということでエラーになります。

ActiveRecordのcallbackの実装を調べたときのメモ

Railsのcallbackの実体

callbackはActiveSupport::Callbacksを使って定義されます。

github.com

ActiveRecordのcallback

ActiveRecord::Callbacksに定義があります。

github.com

ここでcreateなどが定義されます。

github.com

define_model_callbacksは下記で実装されています。

github.com

例 : after_create hookの呼び出され方

ActiveRecord::Baseを継承したUserクラスを考えます。

Railsのバージョンは6.1.4.6を使っています。

Userクラスにはcreateというクラスメソッドがあり、recordを登録できます。

実体は、

irb(main):002:0> User.method(:create)
=> #<Method: User(id: integer, .....).create(attributes=..., &block) /path/to/..../gems/activerecord-6.1.4.6/lib/active_record/persistence.rb:33>

なので、下記です。

github.com

hookがどのように呼び出されるかがわかりにくいので、下記でbreak pointを仕込んでおきます。

github.com

で、after_commit のcallbackをなにかしら設定しておいて、User.createを実行してみます

irb(main):001:0> User.create(....)

[1] pry(User)> 

  TRANSACTION (0.2ms)  BEGIN

From:  /path/to/..../gems/activerecord-6.1.4.6/lib/active_record/callbacks.rb:461 ActiveRecord::Callbacks#_create_record:

    460: def _create_record
 => 461:   binding.pry
    462:   _run_create_callbacks { super }
    463: end

とまりましたね。 ここでcallerを一部抜粋すると

 "/activerecord-6.1.4.6/lib/active_record/callbacks.rb:461:in `_create_record'",
 "/activerecord-6.1.4.6/lib/active_record/timestamp.rb:108:in `_create_record'",
 "/activerecord-6.1.4.6/lib/active_record/persistence.rb:903:in `create_or_update'",
 "/activerecord-6.1.4.6/lib/active_record/callbacks.rb:457:in `block in create_or_update'",

"/activerecord-6.1.4.6/lib/active_record/persistence.rb:477:in `save'"

saveから callbacksの _create_record が呼ばれて、ここでcallbackが発動することとなります。

after_createはcommit前に発動するので、このcallback内で新しくinsertした内容を別のトランザクションからは取得できないので要注意です。

Railsのrspecのrequest specについて

rspec.info

上記の記事にあるように、Rails 5以降ではrails-controller-testing gemをアプリケーションに追加することは推奨されておらず、RSpecコアチームはrequest specsを書くことを推奨しています。

その理由として、request specでは、単一のcontroller actionをテストしつつ、さらにcontroller specとは異なり、router、middleware、rackのrequestとresponseが含まれることになります。

これにより、書いているテストがより実際のリクエストに近くなり、controller testにありがちな問題を回避することができます。

Rails 5ではRails 4のrequest specやcontroller specに比べてかなりの高速化も行われています。

このように、controller specはよりrequest specを書くことがRails 5以降のデファクトとなっています。

GitHub - rspec/rspec-rails: RSpec for Rails 5+

request spec - Request specs - RSpec Rails - RSpec - Relish

request specの使用

rspec-railsを使った場合、specのtypeをtop-levelのdescribeで指定します。

RSpec.describe User, type: :request do

requestを指定することで、ActionDispatch::IntegrationTestに相当することになります。

api.rubyonrails.org

この中でHTTP requestをシュミレートする get post patch put delete head メソッドを使うことになります。

controller specでも同じようなメソッドを使うことになりますが、呼び出されたメソッドが実行するものが違います。

実装は下記になります。

github.com

この中で、 Rack::Test::Session が使われており、Rails appではなく、Rack appとしてリクエストを処理するようになっています。

github.com

request specはRackのレイヤーまでを網羅したテストとなっています。

ちょっとハマった点

rack appに対するリクエストを組み立てテストを行うのですが、Rack::Test:Session#requestの実装が少し色々やりすぎててハマりました

github.com

このなかで、specで定義したparamsをURL-encoded form dataにエンコードを自動的に行なっているので、テストもとでencodeすると二重でencodeされることになります。

こういった処理をどこまでやるかの判断は難しいですが、自動的に行われることは意識しておいていいかなと思います。