はじめて学ぶソフトウェアのテスト技法を読んだ

フィヨルドブートキャンプのプラクティスには、自分で作成したRailsアプリのテストコードを書くプラクティスがあります。 実際にテストコードを書く前に、どのようなテスト技法があるか、TDDとは何か、test-unitについて学習します。 テスト技法について勉強するために、はじめて学ぶソフトウェアのテスト技法を読んだので、まとめていきたいと思います。

www.amazon.co.jp

どのようなテストの種類があるのか?

ブラックボックステスト

用件や仕様に基づいて行うテスト。プログラミング詳細の知識がなくてもテストできる。 ブラックボックステストを行うには仕様から期待する値、期待しない値を選択し、それぞれの入力に対する期待する値を決める。 仕様からサブセットの組み合わせを決定することで、総当たりで入力するデータを組みわせて検証しなくても良い。

ホワイトボックステスト

ソフトウェアの実装、構造に基づいて行うテスト。プログラミングの知識が必要となる。

目指すべきテストケースとは?

全ての可能性を予想してそれらをテストすることは不可能。 最小の時間と労力でほとんどのエラーを検出できるテストケースを目指す。

テストのレベル

単体テスト

ソフトウェアの最小単位をテストする。 何を最小単位とするかはプログラミング言語によって異なる。

統合テスト

単体テストでテストした複数のプログラムを統合させた時に動くかどうかテストする。

システムテスト

もっとも高いレベルの統合時(ソフトウェアが完成した時)に起きる欠陥に焦点を当ててテストする。

受け入れテスト

発注者がソフトウェアを受け入れて、ベンダーに代金を支払うか検証するテスト。 発注者の意図通りに動くか確かめる。

ブラックボックステスト

同値レベルテスト

以下のようなコードがあったとする

def sake
  age = ARGV[0].to_i
  if age < 20
    puts '飲んじゃダメ!'
  else
   puts '飲んでいいよ'
  end
end

sake

$ ruby sake.rb 20
飲んでいいよ
$ ruby sake.rb 10
飲んじゃダメ!

このコードが正しく動くテストを書く場合、n~19までの値全てをテストする必要はない。 この範囲のテストに必要なのは20未満の値ひとつのみ。12を選んでも1を選んでもテストという観点から見れば同じ値である。 ここでいうテストの範囲のこと「同値クラス」と呼ぶ。 「同値クラス」を使ったテストを書くことで、テストケースの数を減らすことができる。

同値クラステストを行うときは、1回に1つの無効値をテストするようにする。1つの無効値に絞ることで、システムが正しく無効値を認識したか確認することができる。

異常値が引数に渡されるパターンもテストするべきなのだろうか? 答えはインターフェースの事前条件によって異なる。 事前条件とはモジュール、メソッドが正常に動作するために期待される条件のこと。 ちなみに事後条件とはメソッド、モジュールが何をするかということ。 事前条件で受け付けるデータがメソッド、モジュール外でしっかり定義されている場合、異常値のテストは実行しなくても問題ない。このようなテストのことを「契約によるテスト」と呼ぶ。 逆にどんな入力でも受け付けるメソッド、モジュールの場合は異常値のテストも実行しなければならない。 上記のプログラムを例にとって考えてみる。 メソッドsake内のage変数はコマンドラインから入力された値を代入しており、どんな入力でも受け付ける状態。 なのでsakeメソッドには異常値が入力された場合の処理、テストを書かないといけない。

境界値テスト

  • データの境界に注目して行うテスト
  • 境界値、境界値のすぐ下、すぐ上のデータをテストする
  • 比較演算子の打ち間違いなどにより境界は欠陥ができやすい

デシジョンテーブルテスト

デシジョンテーブルは、条件の組み合わせに基づいて、条件と対応するアクションを表形式で表したもの。 抜け漏れなく全ての組み合わせをテーブルに格納する必要がある。 テストだけでなく複雑な仕様を確認するのに有効活用できる。

ペア構成テスト

全ての変数の全て値の全ての組み合わせを検証するのではなく、全てのペアの組み合わせを検証する。 ソフトウェアの欠陥のほとんどはシングルモード欠陥、ダブルモード欠陥のいずれかに分類される。 ペア構成テストはシングルモード欠陥、ダブルモード欠陥をカバーする最小の組み合わせを提供する。 直交表か全ペア方式を使ってペアを作成する。

  • 直交表  数字の入った二次元の配列で配列から任意の2列を選ぶと、別のペアをどう選んでも値の全てのペアの組み合わせができる。 qiita.com

  • 全ペア方式  macの場合以下の記事が参考になった。 qiita.com

ペア構成テストはあくまで「全てのペア」の組み合わせをテストするので、シングルモード欠陥、ダブルモード欠陥以外の欠陥はカバーできない。よって特殊な条件がある場合テストを追加する必要がある。

状態遷移テスト

  • 状態遷移図を書くことによって、状態、次の動作、イベントを明確にできる。
  • 状態遷移図と合わせて状態遷移表を使うことで抜け漏れなく遷移を確認できる。

ドメイン分析テスト

  • 複数の変数を同時にテストするための技法。
  • 境界値が不正確に定義、実装された条件を見つけることができる。

ユースケーステスト

  • アクターがどうシステムを使うのシナリオのこと。アクターはユーザーを指すことが多い。シナリオとはユーザーとシステムの相互作用のこと。
  • ユースケーステストはユーザーの視点から定義される。システムの視点からではない。
  • 主成功シナリオに対して少なくとも1つのテストを書く。

ホワイトボックステスト

制御フローテスト

  • モジュール内の実行パスを識別して、それらのパスを網羅するテストを作成する。
  • 制御フローグラフを元にパスを分析する。

データフローテスト

  • 最初に値を代入せずに参照するミスを確認するためデータフローテストを行う。
  • モジュール内の実行パスを識別した後、各パスの使用、未定義の変数のペアをテストする。

テストのパラダイム

スクリプトテスト

  • ソフトウェア開発手法の一つのウォーターフォールモデルの一部分として世に登場した。
  • スクリプトテストとは公式なシステム要求に基づいてテストを計画すること。
  • 再現性、監査性、客観性を担保できる。

探索的テスト

  • 探索的テストとは製品を開発しながらテストの設計と実行を行うこと。
  • 実行すべきてテストケースが事前に決定できず、一つ前のテストの結果を元に次のテストを考えなければならない場合、探索的テストを選択する。

テストの計画

  • スクリプトテストを採用したからといって、探索的テストを取り入れてはいけないということではない。逆も然り。
  • 計画が現在進行形のプロセスである場合、現在の計画は暫定的なものであることを認め、新しい知識や情報が入った時は計画を見直すべきである。

支援方法

欠陥の分類

  • 欠陥を分類することでテストの方向性、重要度を決めることができる。
  • 分類せずにテストすることもできる。

テストの終了判定

テストを終了するか決める時の5つの基準

  • 事前に決めたカバレッジの目標値を達成した
  • 欠陥検出率が、事前に決めた基準以下に下がった
  • 次の欠陥を見つけるのに要する限界コストが、その欠陥で生じると予想される損害額より大きくなった
  • プロジェクトチームが、製品をリリースしても良いという意見に達した
  • 上司による「いいから出荷しろ」の一言

感想

まだまともにテストコードを書いたことがないのでなんのこっちゃと感じる内容が多かったですが、新しいことを学んでいる時によく発生する現象だと割り切って読み進めました。 ひとつ残念な点を挙げるとしたら、章末問題の答えが載っていないことです。 本書を読んで一番よかったと思う点は、正しいテストとの向き合い方を学べたことです。 闇雲、気まぐれにテストを書くのではなく、「最小の時間と労力でほとんどのエラーを検出できるテスト」をかけるエンジニアを目指したいと思いました。 そしてこの本には「最小の時間と労力でほとんどのエラーを検出できるテスト」を書くためのテクニックが詰まっています。 どんなテストを書けばいいんだ?と迷った時、このテストは何のためにあるんだろうと疑問に思った時、読み返したい一冊です。

ユーザーフォローの関連付けを自分の言葉で説明してみる

Rails Tutorial 14章でお馴染みのユーザーフォロー機能。 ユーザー同士の多対多と関係するコードについて、自分の言葉で説明してみたいと思います。 尚、実装例や全ての解説とコードは以下の記事にまとまっていますので、とりあえず動かしたいという方はそちらを読んでください。

qiita.com

ユーザーフォロー概観

  • 1ユーザーはたくさんのユーザーをフォローでき、たくさんのユーザーによってフォローされる
  • このような関係のことを自己結合多対多と呼ぶ
  • 多対多なので、中間テーブル(Relationships)が必要

ゴール

irb(main):002:0> user.followings
=> #<ActiveRecord::Associations::CollectionProxy [#<User id: 3, email: "testuser-3@example.com", created_at: "2020-08-11 00:05:15", updated_at: "2020-08-11 00:05:15", address: nil, postal_code: nil, bio: nil, name: "testuser-3", uid: "d36fa9e5-c0ba-41b0-a386-1122be7e8bde", provider: "">, #<User id: 4, email: "testuser-4@example.com", created_at: "2020-08-11 00:05:16", updated_at: "2020-08-11 00:05:16", address: nil, postal_code: nil, bio: nil, name: "testuser-4", uid: "4a93e555-40ec-4308-a3d5-c2a59ae03c2e", provider: "">]>

irb(main):004:0> user.followers
=> #<ActiveRecord::Associations::CollectionProxy [#<User id: 2, email: "testuser-2@example.com", created_at: "2020-08-11 00:05:15", updated_at: "2020-08-11 00:05:15", address: nil, postal_code: nil, bio: nil, name: "testuser-2", uid: "a927f181-595e-4e74-9c29-11367612cb85", provider: "">, #<User id: 3, email: "testuser-3@example.com", created_at: "2020-08-11 00:05:15", updated_at: "2020-08-11 00:05:15", address: nil, postal_code: nil, bio: nil, name: "testuser-3", uid: "d36fa9e5-c0ba-41b0-a386-1122be7e8bde", provider: "">]>

自分がフォローしているユーザー、フォローされているユーザーを取得できるようにします。

ユーザーモデルの切り分け

Railsのコードを書く前に、DBレベルではどのようなデータが保存されているか確認します。(手書きのメモが汚いです すんません..)

f:id:mashoo1101:20200811163710p:plain とてもシンプルですね。 ユーザーはユーザーをたくさん持っていて、ユーザーはユーザーをたくさん持っている状態を表しています。 DBレベルではデータを保存するだけなのでこの実装で問題ないのですが、Rails側でデータを取り出したいとなった時にひとつ困ることがあります。 「ユーザーはユーザーをたくさん持っていて、ユーザーはユーザーをたくさん持っている」これだけだと、ユーザー間の関係性をうまく表現できません。フォロワーとフォローされる側を明確にする必要があります。 よって以下のコードを追記します。

class Relationship < ApplicationRecord
  belongs_to :following, class_name: 'User'
  belongs_to :follower, class_name: 'User'
end

この記述により、Railsからは以下のようにデータを扱えるようになりました。

f:id:mashoo1101:20200811163954p:plain

「フォロワーはたくさんのフォローしているユーザーを持っている」&「フォローしているユーザーはたくさんのフォローされているユーザーを持っている」という状態を表現できました。 この記述により最後に実装する「ユーザーの取得」で、適切なユーザーを取得できるようになります。 (あくまでRailsから操作できる仮想的なモデル名であり、DBに保存される情報はコード追記前と変わりません)

Relationshipから適切なデータを取得する

最終目標である「自分がフォローしているユーザー、フォローされているユーザーを取得」するには、中間テーブルであるRelationshipsを使えば書けそうです。 現在の中間テーブルの中身は以下のようになっています。

f:id:mashoo1101:20200811163612p:plain

この中から自分をフォローしているユーザーとの関係(relationship)を表す列を取得するにはどうすれば良いでしょうか? 答えは 「follower_idの値が自分のidの値と同じ列」を取得するよう指定する必要があります。 逆に自分がフォローしているユーザーとの関係(relationship)を表す列を取得するには、「following_idの値が自分のidの値と同じ列」を取得するよう指定する必要があります。 UserをFollowerとFollowingに分けたように上記二つに名前を付けてあげましょう。 自分をフォローしているユーザーとの関係を「passive_relationships」、自分がフォローしているユーザーとの関係を「active_relationships」とします。 コードは以下のようになります。

class User < ApplicationRecord
   has_many :active_relationships, class_name: 'Relationship', foreign_key: :following_id, dependent: :destroy
   has_many :passive_relationships, class_name: 'Relationship', foreign_key: :follower_id, dependent: :destroy
end

「follower_idの値が自分のidの値と同じ列」を取得する指定はforeign_keyオプションで行います。 dependent: :destroyはユーザーが削除された時、relationshipも削除するという意味になります。

ユーザーの取得

active_relationshipspassive_relationshipsを通してフォロー中/フォロワーのユーザーを取得します。 followingsを例に説明します。自分がフォローしているユーザーとの関係性を表すactive_relationshipsから、自分にフォローされているユーザーを取得します。 コードは以下のようになります

class User < ApplicationRecord
  has_many :followings, through: :active_relationships, source: :follower
  has_many :followers, through: :passive_relationships, source: :following
end

「ユーザーモデルの切り分け」で記述した以下のコードによってどちらのユーザーを取ってくるかsourceを指定できるのです!!

class Relationship < ApplicationRecord
  belongs_to :following, class_name: 'User'
  belongs_to :follower, class_name: 'User'
end

 まとめ

既にユーザーフォローに関する素晴らしい記事は存在しますが、自分の言葉でまとめてみると理解度も上がるなと感じました。 疑問点、間違いの指摘等ありましたらコメントお願いします。

Error: Duplicate column name を解決する

rails db:migrationした時に、Error: Duplicate column name "fuga"というエラーが出た時の解決方法。 エラーにある通り、fugaというカラムはダブってまっせ〜ということです。 まず現在のDBのバージョンを確認。

$ rails db:migrate:status

database: db/development.sqlite3

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20200707094922  Create books
   up     20200708020232  Add picture to books
   up     20200718144222  Devise create users
   drop  20200718161232  Add column to users
   up     20200802141002  Add omniauth columns to users
   up     20200802143752  Add index to users
   up     20200808064935  Create relationships
   up     20200809072542  Add not null to relationship

Add column to usersに対応するマイグレーションファイルが実行できていない。

次にdb/schema.rbを確認。

ActiveRecord::Schema.define(version: 2020_08_09_072542) do
   # 省略
    create_table "users", force: :cascade do |t|
       t.string "name"
    end
end

既にUserのnameというカラムは作成されていることを確認できる。 つまりマイグレーションの実行結果は既にDBに反映されているため、同じマイグレーションを実行することはできないということ。 よって、マイグレーションファイルを以下のように修正し、rails db:migrateする。

class AddColumnsToUsers < ActiveRecord::Migration[6.0]
  def change
  # 何も書かない
  end
end

これで全てのマイグレーションを実行できた。

$ rails db:migrate:status

database: db/development.sqlite3

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20200707094922  Create books
   up     20200708020232  Add picture to books
   up     20200718144222  Devise create users
   up     20200718161232  Add column to users
   up     20200802141002  Add omniauth columns to users
   up     20200802143752  Add index to users
   up     20200804145226  Create active storage tablesactive storage
   up     20200808064935  Create relationships
   up     20200809072542  Add not null to relationship

修正したマイグレーションファイルは、元の状態に戻しておく。

Rails6 Webpacker::Manifest::MissingEntryErrorを解決する

環境

起きたこと

rails g controller index homeして、ページにアクセスしたところ以下のエラーが出た。

=> Booting Puma
=> Rails 6.0.3.2 application starting in development 
=> Run `rails server --help` for more startup options
Puma starting in single mode...
* Version 4.3.5 (ruby 2.7.1-p83), codename: Mysterious Traveller
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://127.0.0.1:3000
* Listening on tcp://[::1]:3000
Use Ctrl-C to stop
Started GET "/hello/index" for ::1 at 2020-07-07 16:46:25 +0900
   (1.6ms)  SELECT sqlite_version(*)
Processing by HelloController#index as HTML
  Rendering hello/index.html.erb within layouts/application
  Rendered hello/index.html.erb within layouts/application (Duration: 2.3ms | Allocations: 189)
/Users/mashio/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/sprockets-rails-3.2.1/lib/sprockets/rails/helper.rb:355: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call
/Users/mashio/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/sprockets-4.0.2/lib/sprockets/base.rb:118: warning: The called method `[]' is defined here
[Webpacker] Compiling...
[Webpacker] Compilation failed:
yarn run v1.22.4
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.


error Command "webpack" not found.

Completed 500 Internal Server Error in 2640ms (ActiveRecord: 0.0ms | Allocations: 9336)

ActionView::Template::Error (Webpacker can't find application in /Users/mashio/Desktop/fjord/rails/helloworld/public/packs/manifest.json. Possible causes:
1. You want to set webpacker.yml value of compile to true for your environment
   unless you are using the `webpack -w` or the webpack-dev-server.
2. webpack has not yet re-run to reflect updates.
3. You have misconfigured Webpacker's config/webpacker.yml file.
4. Your webpack configuration is not creating a manifest.
Your manifest contains:
{
}
):
     6:     <%= csp_meta_tag %>
     7: 
     8:     <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
     9:     <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
    10:   </head>
    11: 
    12:   <body>

error Command "webpack" not found. ここが気になる.. 一度rails webpacker:installしてちゃんとインストールされるのか、確認したところ気になる部分を見つけた。

error browserslist@4.13.0: The engine "node" is incompatible with this module. Expected version "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7". Got "13.6.0"
error Found incompatible module.

nodeのバージョンが原因でエラーが出ている。

nodebrewを使って、別のバージョンのnodeをインストールし有効化してみる。

$ nodebrew install v12.0.0

$ nodebrew use v12.0.0

再度webpackerをインストールすると、errorが出ずにインストールできる。

そしてrails sすると...

f:id:mashoo1101:20200707180703p:plain

無事表示できました🎊

さくらVPS上のPostgreSQLにsshポートフォワーディングで接続する

なぜポートフォワーディングで接続するのか

先日MacからさくらVPSにあるPostgreSQLのデータベースに接続してみました。が、 外部接続の設定が誰でも接続できる設定にした(サブネットマスクが0)のがずっと気になっていました。

izumii19.hatenablog.com

ポートフォワーディングを使うことで、安全に接続できることを知ったのでまとめてみます。

ポートフォワーディングとは

http://e-words.jp/w/%E3%83%9D%E3%83%BC%E3%83%88%E3%83%95%E3%82%A9%E3%83%AF%E3%83%BC%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0.html にはこうあります。

ポートフォワーディングとは、IPネットワーク上のある機器が、自らのIPアドレスTCPUDPの特定のポート番号への通信を、別のアドレスの特定のポートへ自動的に転送すること。

これを使うことで、MacからVPSのPostgrSQLへ直接接続しているかのように操作できます。 現時点で自分が知っている接続方法を挙げてみます。

  1. VPSSSH接続し、PostgreSQLに接続する(Mac--->VPS--->PostgreSQL)

  2. PostgreSQLに外部からの通信を受け付ける設定をして、Macから直接PostgreSQLに接続する(Mac--->PostgreSQL)

  3. ポートフォワーディングの設定をし、MacからPostgreSQLに接続する (Mac--->(VPS)--->PostgreSQL)

今回試したのは3つ目のポートフォワーディングを使った接続です。 ポートフォワーディングはどちらかというと1のSSHに近いなとやってみて思いました。 この記事のリモートフォワードのイラストがとてもわかりやすかったです。 https://qiita.com/mechamogera/items/b1bb9130273deb9426f5

localhostとは

設定ファイルでよく見るlocalhostについて調べました。 loaclhostは自身を表すホスト名のことです。loaclhostを使うとTCP/IP ソケットの場合以下のように接続できます。 psql -h localhost postgres -p 5432 なんでTCP/IPを使うかというと、ユニックスドメインソケットによる接続はPostgreSQL が動いているマシンに直接ログイン(例えばSSH)しなければならないので、今回のような外部接続の場合、TCP/IPを使うことになります。 以下の記事の説明はとてもわかりやすかったです。 http://www.hizlab.net/app/pgsec.html

トンネルを掘る⛏

コマンド一本でポートフォワーディングの設定は終わります。 ポートフォワーディングを設定することを、「トンネルを掘る」と呼ぶそうです。 わかりやすい...! 実行したコマンドは以下の通りです。

ssh -N -L [ローカル側で転送に使用するPort)]:[PostgreSQLのHostName]:[PostgreSQLが解放しているPort(デフォルトだと5432)] -i [さくらVPSの秘密鍵])] -p [VPSが解放しているPort] [VPSのUser]@[VPSのIPアドレス]

PostgreSQLのHostNameはlocalhostを指定します。 VPSのポート、ユーザー名、ホスト名は普段SSHで使っているのでOKです。 これで接続の準備が整いました。-fオプションをつけるとバックグラウンドで実行できます。 接続するには以下のコマンドを実行します。

psql -h localhost -p [ローカル側で転送に使用するPort] -U [ユーザー名] -d [DB名]

接続完了!

不要な設定を削除する

接続はできたものの、不要な設定が残っていないか見直してみました。 外部接続に関するファイルは2つあります。

/etc/postgresql/12/main/postgresql.conf

listen_addresses = 'localhost'    

listen_address = '*'は「何でもOK!どうぞどうぞ」な設定なので変更。 listen_addresについて

クライアント認証 は誰がサーバにアクセス可能かをきめ細かく制御するのに対し、listen_addressesはどのインターフェイスが接続を試みるかを制御します。

listen_addressにどのインターフェースを受け付けるかのざっくりとした設定を書き、クラインアント認証(pg_hba.conf)でより詳細な設定を書くようです。

ちなみに、listen_addressで設定されていないことによって弾かれた時のメッセージと、listen_addressでは許可されているが、pg_hba.confでは設定されていない時に表示されるメッセージが違うことを確認しました。 - listen_addressで設定されていない時のメッセージ image.png

  • pg_hba.confで設定されていない時のメッセージ image.png

/etc/postgresql/12/main/pg_hba.conf

# Database administrative login by Unix domain socket
# TYPE  DATABASE        USER            ADDRESS                 METHOD

# "local" is for Unix domain socket connections only
  local   all             all                                     peer
# IPv4 local connections:
  host    all             all             127.0.0.1/32            md5
# IPv6 local connections:
 host    all             all             ::1/128                 md5
# Allow replication connections from localhost, by a user with the
# replication privilege.
 local   replication     all                                     peer
 host    replication     all             127.0.0.1/32            md5
 host    replication     all             ::1/128                 md5

hostの設定はlocalhost(自身のIPアドレス)のみとし、ポートフォワーディングで使用する接続以外は受け付けないようにしました。 psql -U ユーザー名 -d postgres -h さくらVPSのホスト名みたいな接続は受け付けません。 ユニックスドメインソケットの設定と、リプレーションの設定はそのままにしておきました。

参考記事(状況が全く同じだったためとても参考になりました) https://qiita.com/rjge/items/d9ec5eb463a0ce24cb8

感想

設定ファイルの意味などを調べつつ動作確認をしたので時間がかかりましたが、自分が何をしているのか&してきたのかをだいたい理解できました。 また何でpeer認証なんてあるんだ?なぜpostgresっていうユーザーがOSのユーザーとして作成されるんだという疑問も解決しました。 「UNIX ドメインソケット経由の場合、アクセス制御はOSレベルに任せることで、安全に運用できるから」となりました。 逆にOSレベルにユーザー管理任せないでどうするの?と考えたところ、そっか!OSに任せた方が都合いいかと納得しました。 読んでてSSHとあんまり変わらないじゃん!と思った方... ポートフォワーディングの方がかっこいいと僕は思います!

プロセスとジョブの違い

コマンドを実行する時、内部の動きは以下のようになる。

  1. シェルからコマンドを実行

  2. カーネルはディスクから実行ファイルを読み出してメモリに入れる

  3. メモリ内容に従ってCPUがプログラムを実行する

プロセス
カーネルから見た処理の単位。 メモリ上で実行状態にあるプログラムのことを「プロセス」と呼ぶ。 「プロセス」にはLinuxのシステム全体で一意のIDがつけられる。
ジョブ
シェルから見た処理の単位(使う人間側から見た時の単位)。 シェルのコマンドラインに入力している1つの行が、1つのジョブに該当する。 ジョブはシェルごとにジョブ番号を持っている。

プロセスIDはlinuxのシステム全体で一意のIDだが、ジョブ番号は「シェル」の中で一意のIDなので、複数ターミナルを立ち上げている場合、同じジョブ番号を他のシェルから確認できる。(重複する)