やる気がストロングZERO

やる気のストロングスタイル

RSpecでmockを使ってテストする

こういう時にmockを使う

あるメソッドのテストコードを書きたい。
そのメソッドは内部で外部システムと通信をするclass(以下apiクラスと呼ぶ)を使っているため、実行のたびに結果が異なるのでテストがかけない。
apiクラスが返してくる値を決めてしまいたい。←コレをmockで解決する

前提コード

MyClass#some_infoをテストしたいが、ApiClass#some_dataがAを返すかBを返すかが状況で変わってくるのでテストできない。

class ApiClass
  def some_data()
    # 外部apiにアクセスしてデータを取ってくる(”A" or “B” を返す)
  end
end

class MyClass
  def initialize
    @api = ApiClass.new
  end

  def some_info
    data = @api.some_data
    case data
    when "A"
        return "aでした"
    when "B"
        return "bでした"
    end
  end
end

ApiClassのmockを作ってテストする

describe 'moc練習' do
  before do
    @api_moc = double("ApiClass moc") # ApiClassのインスタンスのmocを作成
  end

  context "ApiClass#some_dataがAを返す場合" do
    before do
      allow(@api_moc).to receive(:some_data).and_return("A") #@api_moc.some_data()が"A"を返すように設定
      allow(ApiClass).to receive(:new).and_return(@api_moc) #ApiClass.newがapi_mocを返すように設定
    end

    it '[aでした]が返る' do
      my_class = MyClass.new
      expect(my_class.some_info).to eq "aでした"
    end
  end

  context "ApiClass#some_dataがBを返す場合" do
    before do
      allow(@api_moc).to receive(:some_data).and_return("B") #@api_moc.some_data()が"B"を返すように設定
      allow(ApiClass).to receive(:new).and_return(@api_moc) #ApiClass.newがapi_mocを返すように設定
    end

    it '[bでした]が返る' do
      my_class = MyClass.new
      expect(my_class.some_info).to eq "bでした"
    end
  end
end

他にも、、

expect(@api_moc).to receive(:some_data) でsome_dataが呼び出されなければNGとしたり、

allow(@api_moc).to receive(:some_data).and_raise('エラーが発生しました’)
で例外を発生させたり、

呼び出し回数をチェックしたり、
引数をチェックしたりもできるらしい。

参考)

使えるRSpec入門・その3「ゼロからわかるモック(mock)を使ったテストの書き方」 - Qiita

Cypressの記法メモ

cypressとは?

https://www.cypress.io/

ブラウザを操作してwebアプリのテストを自動実行できる。
seleniumとかと同じようなカテゴリに属すると思う。(ただし、クロスブラウザテストは現状できない)
seleniumよりも環境構築が簡単で気軽に試せる感じ。npm install cypress
webのelementの取得周りもけっこう使いやすい印象だった。

インストールや実行についてはこのあたりを参考に。

Installing Cypress | Cypress Documentation

cypressを触ってみた - Qiita

まだ全然使いこなせていないが、やったことをメモしとく。

テストの書き方

Rspecっぽく書ける。

cypress/integration/text_spec.js

describe('ログイン画面', () => {
    beforeEach(() => {
        cy.login("username", "password") // 前もってログイン(ログインコマンドを定義している。詳しくは下述)
    })

    it('ユーザー名が表示されている', () => {
         expect(cy.contains('username')).to.exist // usernameが表示されているかを確認
    })

    afterEach(() => {
        cy.logout() // テスト完了後にログアウト
    })
})

要素の取得の仕方

contains | Cypress Documentation

例えば、以下の要素を取得したければ

<button>押せ!</button>

以下の感じで取得できる。

cy.contains('押せ!')

もし「押せ!」が複数要素ある場合は最初のものが取得されるので、 以下のようにgetにcssセレクタを渡して絞り込んで目的の要素を取得する

cy.get('form').contains('押せ!')

ボタンクリックの仕方

ボタン要素を取得してclick()

cy.get('form').contains('押せ!').click()

text要素のチェックの仕方

特定の要素のtextをcheckしたい場合は以下のような感じ

cy.get("#id > p:nth-child(3)").should(($el) =>{
    expect($el).to.have.text("予期されているテキスト")
})

スクリーンショットのとり方

cy.screenshot('ファイル名')

画面遷移してすぐにスクショを実行すると、まだ描画中のスクショがとれてしまっていたりする。
以下のようにすると、「申し込む」の要素が取得できたタイミングでスクショが実行される。

cy.get('form').contains('申込む').then(() => {
    cy.screenshot('ファイル名')
})

cyメソッドの定義の仕方

cypress/support/commands.js に以下のように書くと、cy.login("username", "password")のように使えるようになる。

Cypress.Commands.add('login', (username, password) => {
    cy.visit(`/login`)
    cy.get('input[name="account[username]"]').type(username)
    cy.get('input[name="account[password]"]').type(password)
    cy.get('input').contains('ログイン').click()
})

モジュールの作成の仕方

Cypressではなくjsの範疇の話だけど、別ファイルに分割してimportしたい時は以下の感じ。

cypress/util/greet.js

export default class Greet{
    static greeting(){
        console.log("hello")
    }
}

cypress/integration/text_spec.js

import Greet from '../util/greet'
describe('importテスト', () => {
    it('Greetが実行できる', () => {
         Greet.greeting() // consoleにhelloが表示される
    })
})

バグ原因調査の手順を考える

バグ原因調査の手順を考える

ログを確認する

ログを確認するとかなり原因を絞り込める。
(エラーが起きていないか確認したり、想定されているアクセスが発生しているのかの確認など。)

なんかのバッチ処理とかの場合、
だいたい「~~処理開始」とかがログに書き込まれているのでそれを足がかりにしたり、
“Started POST “~~~" for 127.0.0.1 at 2019-04-04 17:19:42 +0900”
みたいなのを足がかりして確認し、どのあたりのコードが怪しいのかを確認する。

ログファイルは段落分けとかされてなくてすぐどこを見ているのかわからなくなるので、 grepなどをつかって見やすいように工夫する。

運用や調査でログを確認するときによく使うコマンド集 - Qiita

↓この辺が便利そうな予感

grep [string] -A N  
該当する行のN行後も出力

grep [string] -B N  
該当する行のN行前から出力

grep [string] -C N  
該当する行の前後N行出力

railsのログの場合、lessで内容をみると"ESC[1m”とかが表示されて見にくかったが、なんか表示カラーリングのための情報らしくてless -Rで表示させるときれいに表示できた。

コードを確認する

原因が馴染みの無いコード部分の場合

初めて目にする部分であった場合などはそもそも何をやっている部分なのかわからなかったりする。

局所的にコード詳細を追っていっても迷子になってなかなか理解できないので、 全体像から始めて徐々に詳細を理解していくようにする。

非エンジニア職からエンジニア職になるには

僕はもともとエンジニアではないが、現在エンジニアとして働いている。
その経験から得た「日曜エンジニアから職業エンジニアになる方法」を書いてみる。

注意)諸説あり。個人差があり。

前提:プログラミングが好きであること。

・お金が発生しなくてもできる。
・職業にできなくても趣味として続けられる。

くらい好きであること。

第一段階

非エンジニア職でエンジニアが働いている会社に就職する。

例えば、
・テスター
・ユーザーサポート
webデザイナー
など。

その職で、その会社のプロダクトや業務に精通する。

第二段階

プロダクトや業務について、エンジニアよりも詳しくなった状態で、
趣味でプログラミングやっててある程度できることを普段からアッピールする。

アッピール用のアウトプット大事
・資格
github
・ブログ
・個人プロジェクト回している
など

第三段階

簡単なプログラミングの仕事をもらってやり遂げる。(未経験からの脱却)

第四段階

なんかの機会に 配置替え・転職などしてエンジニア職になる。

心構え

周りの凄い人と比較して自分の知らなさ出来なさにいちいち落ち込まないようにする。
自分は自分の出来ることをやっていきながら、少しずつでもできることを増やしていく。

ModelにApplicationが提供する機能を実装するとつらくなる

どういうこと?

railsを例にとるが例えばアカウント登録機能を実装するとする。
アカウント登録が行われたらユーザーに完了メールが飛ぶという仕様。

コントローラーに処理を書くのは良くないので、Modelに実装された。 save()が実行されるとメールが飛ぶように実装された。

class Account < ActiveRecord::Base
 
  after_save do
    # メールを送信する
  end
 
end

コントローラーからはこんな感じで使われる。

account = Account.new(data)
account.save()

アカウント登録が実行されると忘れずにメールが飛ぶので便利。

なにが問題か?

仕様の把握が難解になる

新しくアサインされた開発者は

account = Account.new(data)
account.save()

ここを見てメールが送信されるとは読み取れない。
この関数を使った機能を追加して、意図しないタイミングでユーザーへメールが飛んでしまうバグを仕込んでしまう可能性が大きい。

機能追加の難易度が上がる

例えば、メールアドレスを後から登録することも可能とすることになったとする。
現状Account.save()を実行するとメール送信の処理が実行されるのですんなりと対応できない。

こんな感じのif分岐が内部に作られるかもしれない。

class Account < ActiveRecord::Base
 
  after_save do
    unless mail_address.nil? # mail_addressがnullでなけば
        # メール送信
    end
  end
 
end

とりあえず機能追加はできたが、この対応はコードの複雑さを更に上昇させてしまっているので個人的にはナシ。

コードが仕様を表現していない。
「メールアドレスを保持してなければ、メールを送らない」と書かれているだけで、どういうときにこのパターンが発生するのかがコードから読み取れない。

仕様としては
「1:通常のアカウント登録パターン」と
「2:メールアドレスを後から登録するアカウント登録パターン」があって、
2:のパターンの場合、もちろんメール送信はしないという話。

仮に「(1:)のパターンでのアカウント登録処理の実行中なのにメールアドレスが入力されていない状態」でaccount.save()が実行された場合、本来ならエラーにしたりしないといけないが、上記では登録されてしまうというバグにつながるかもしれない。
その可能性にも気が付けない。

なんでこんなことになってるのか?

「単一責任の原則」が守られていない。

account.save()

は名前どおりaccountが保持するデータの永続化処理のみを提供すべき。 そこにメール送信処理が混入しているのが問題。

そもそもAccountモデルはaccoutsテーブルを対象に操作を行うのが責務で、それ以外(メール送信処理)を行うのは責務が混在している。

どうすべきか?

Accountモデルはaccoutsテーブルを対象とした操作のみを行うようにする。

class Account < ActiveRecord::Base
 
  after_save do
    # なにもしない。保存だけを行うように変更した。
  end
 
end

Service層を設けてこんな関数を定義する

class AccountService
    def create_account(data)
        account = account.new(data)
        account.save()
        MailService.send(account.mail_address, '登録完了しました')
    end
end

メールアドレスを後から登録することも可能とすることになったら、 こんな感じにメソッドを追加。

class AccountService
    def create_account(data)
        account = account.new(data)
        account.save()
        MailService.send(account.mail_address, '登録完了しました')
    end

    def create_account_without_mail_address(data)
        account = account.new(data)
        account.save()
    end
end

それぞれ適切なバリデーションを設定することもやりやすい。

こんな感じで、
「Modelは担当するDBレコードの操作のみを行い、Serviceがそれらを組み合わせてApplicationが提供する機能を実現する」
というのがいいのではないかと思っている。

ActiveRecordのafter_saveとかは便利と思うけど、 after_saveが他のmodelの保存をおこなって、そのmodelのafter_saveでさらに別のmodelの保存を、、とかなるとかなり辛いのでafter_saveとかあまり使いたくない派。

新規開発計画アンチパターン:画面毎担当(期限付き)

エンジニア職ではない人が新規システム開発の計画をすると、必要な画面一覧を洗い出して画面毎に期限を設定した計画を作成される場合がある。

この計画をそのまま受け入れて進めてしまう事自体がアンチパターンだけど、
力関係など様々な原因で通ってしまう場合もある。

なぜアンチパターン

同じ機能の別実装が多数作成される

画面毎に期限が指定されているので、その画面を動かすことが最優先事項になり、共通化に時間が使われなくなる。
綺麗に共通化したモジュール作成に時間を使っても画面が動いていないと進捗会議で出せるものがなくなる為、共通化への動機が薄くなる。

さらに、エンジニアレベルにもよるけど、同じ機能が画面毎に、画面に密結合した形で実装されがちになる。
※同じデータのデータ取得でさえ画面毎に実装され、データ構造も画面毎に実装されている例を見た事がある。

結果、
同じ機能の別実装(画面に密結合)が多数存在することになる。
仕様変更などで機能に修正が入る際、全ての機能実装に対して修正を入れないといけない。
全ての実装が微妙にずれた状態になったりする。どれが真かわからなくなる。

中途半端に実装された画面が多数作成され、進捗が不明になる。

画面には多数の機能が含まれている。(検索機能、編集機能、新規登録機能、入力サポート機能などなど)

例えば、この画面はまだ「新規登録機能」しか実装されていないが、別の画面の作成に「新規登録機能」が必要である場合に、とりあえずこの状態でマージされたりする。

こういったことが繰り返され、どの画面がどこまで実装されているのかわからなくなっていき、残作業が見えなくなっていく。

何がどこまで出来ているのか不明になると、コードを読み解いて状態を認識する作業が発生し、無限に時間が飲み込まれていく。

どうすべきか

画面に注目せず機能に注目して計画を立てる。

必要な機能を洗い出し、依存関係を整理して機能毎に実装を進める。
同じ機能の複数実装を許さない。
画面の実装は後回し。 動作テストなどで必要なら簡易に実装しておく。

入社直後の既存システム把握で奮闘

転職して新しいシステムを扱うので把握しないといけない。
把握を進めていく方法で悩んで色々試しているので書いてみる。

なぜシステムの把握が必要か?

なにをやるにしても全体像が把握できていないと辛い。
新機能追加の際にも、合わせて調整しないといけない部分が洗い出せない 等。。

トライした方法とその効果など

◆コードリーディングでの把握
あまり有効でがなかった気がする。
時間がかかりすぎる
枝葉情報が多すぎる
まとめきれない
細かい情報はまたきっとまた忘れる
システムの主機能のメインルートの流れを把握するのは有用だったと思う。
そのアプリケーションのコードの書き方やコードの配置がざっくりわかる程度には溺れておいてよかった気がする。

◆モデル構造・DBテーブル構造からの把握
有効だったと思う。
ざっくりとどのようなエンティティがあるのかが把握できた。
テーブルカラム内容一つ一つ追うのではなく、ER図とともに関連をざっくり把握するのがよさげ。
テーブル構造をざっくり把握しておくと、その後の業務説明を口頭で受ける際にも最終的にどのテーブルにデータが入るのかを考えながら聞けるので、理解しやすくなった。
DataGripだとダイアグラムを生成できるので把握しやすかった。
今回はrailsだったのだが、モデル図を生成するgemがあったのでER図を生成できた。これも全体像を把握するのに役立った。
DataGrip使うと気になったテーブル内容をパパっと確認できたのでよかった。(shift2連打でテーブル名入力)
引きの速さは理解速度に影響しそう。
※他のクライアントあんまりつかったことないけど。

◆口頭説明されたことを細かくメモ
局所的にかなり詳しい仕様を聞けるので有効だが、機会が限定的なのであまりこればかりに頼れない感じ。
また、入社直後は全体像がわかっていないかつ、与えられる情報が多すぎて受け止めきれない場合が多いのである程度わかってから再度聞きたい感じ。
聞いたことを一回書き出すことで脳内整理する。が、メモ自体はあまり役に立たない気がする。(まとめ方を考えて正式にどこかにまとめない限り)

API一覧から把握
ある程度有効。
どういう機能があるのかを網羅的に眺めることができる。
一つ一つ細かくではなく、ある程度のカテゴリ別に確認して、どういう機能が存在しているのかをなんとなく把握できる。

◆画面から把握
情報整理の方法が確立できていないと理解の効率が悪かった。漠然と眺めている感じになってた。
ただし、詳しく調査したい機能がある場合、まず「どういった画面になっているか?」から入るのは個人的に理解が進みやすい。

最近ググって良さそうな記事を発見

既存システムを分析するコツは「システムの地図」を作ること (1/3):CodeZine(コードジン)

・全体像からざっくり把握する
・把握する際の視点
・情報を残す際の手法

みたいなことが書かれている。

今後しばらくこれを参考に進めてみるつもり。