kazumalab tech log

流行りとリラックマと嵐が大好きです。技術的ログ。

テストについて

E2Eテスト

今日ちょっとE2Eテストについて説明してみてって上司から言われ説明してみたら0点いただきました。Wow...ちゃんと勉強しよ。
インテグレーションテストが間違ってたっぽい。
E2Eのテストとインテグレーションテストがごちゃっとしていて、それじゃおかしくなるとの指摘。なんとなくだけど久しぶりに詰められた気がする。
詰められるの嫌だけど、だめなところをしっかり痛いところをついて指摘してくれるのはすごくありがたい。

「3年目でこれはやばくないか?」

まじでそれ。やればやるほどコンピュータの世界知ることが多い。
一つづつ着実に身に着けて行かないと上辺だけでやってたら死ぬと最近思っていた矢先だったので、いい機会だったと思う。

  • Unitテストはクラス単体のテスト
  • Integrationテストはクラス同士の結合テスト
  • E2Eはユーザーケースを自動化したテスト

じゃあRailsのModelの単体テストはUnitテストなのか?という議題に関して、概念的にはNoらしい。
しかし事実上はYesとなっていて、そもそもRailsのModelはActiveRecordを継承しているので、そもそもUnitテストじゃなくなる。
でもそれをUnitテストってことにしちゃおう、みたいな流れ。

Unitテストをガチガチにやるなら別クラスのものはMockとStubを使ってテストする。
Railsだと厳しい感じある。

Controllerテストはどうか、あれはIntegrationの役割をしているが、render_viewsを時折利用していて、Viewと密になっているケースがある。
まぁなにわともあれ、Unitテスト、Integrationテスト、そしてE2Eのテストを書こうね、そしてそれぞれが持つ役割をちゃんと理解しておこうねという話。

RailsだとE2EはFeatureSpecか、SystemSpec。
最近ChromeDriverが使えるようになって前よりも安定してきている気がしている。

最後に、もう一度ちゃんと初めての自動テストを読むことにしてPDFを購入して、1章を読み終えたところだ。

www.oreilly.co.jp

npm first release

npmのpackageをリリースした。その名も rcolor.js

github.com

最近ツヨツヨなエンジニアはみんなライブラリを公開しているっぽいので、ツヨツヨを目指すべくまずは初歩としてライブラリを公開してみたのが始まり。APIとしては単純で、カラーコード(hex)をStringで渡すと反転色を返してくれるというLibrary。Reactとかで最近DOMのStyleを直接弄ったりすることが多くて、ちょっとどうにかならないかなーと思って作った。(世の中にいっぱいある)

最近yarnのほうがよく使うので、自分のプロジェクトでyarnでinstallできればよきかなと思ったが、思った以上にかんたんだった。

今回はES6のclass構文を使って書いた。テストはFacebookが提供しているJest。 使い方はシンプル。

yarn add rcolor.js
import RColor from 'rcolor.js'

const rcolor = new RColor('#ffffff')
rcolor.toReverse() // #000000

www.npmjs.com

packageの公開もすごく簡単だった。公式がおすすめ。

yarnpkg.com yarnpkg.com

Firebase Day1

Firebase、今いろんなところで使われているGoogleが提供しているサービス。 昔むかしにUnityのデータ保存先としてRealtimeDatabaseを使ったことがあるが、もうわすれてしまった。 最近はReactとかにふれる機会が多いので、それをFirebaseのhostingに乗っけて見ようかと思った、ちょっとお試ししてみた。

なんかReactのプロジェクトをいい感じで作ってくれるコマンドがあるのでそれを打って見る。

$ npx create-react-app colors

今回は適当に色を組み合わせて保存するサービスを作ろうかなぁと思ったので、colorsにした。 まぁ特に意味はないんだけど。

$ cd colors
$ firebase init

firebase-cliは別途インストールした。 initで対話形式でプロジェクトに必要なjsonを吐き出してくれる。なるほど。 ちなみに、プロジェクトをFirebase上で作って、リージョンを指定しないとプロジェクト選択でエラーが出るのでWebでポチポチっと。 なるほどなぁ。 そこはこの上ではできないんだ..って感じ。その後、デプロイしてみる。

$ npm run build
$ firebase deploy

この2コマンドぐらいで行けるが、

Error: Cloud resource location is not set for this project but the operation you are attempting to perform in Cloud Firestore requires it

有効になっていないリソースがあればエラーになるっぽい。Web上でポチポチっと有効にしてみたらとりあえずOK。 今日はその後firebase Authenticationを利用してGoogleOauthのログインまで完了した。

FirebaseのAuthenticationを利用するところはこういう感じに書いた。

import React from 'react'
import './App.css';
import firebase from "./firebase-config"

class App extends React.Component {
  constructor(props) {
    super(props)

    this.state = {
      user: null
    }
  }

  render() {
    return(
      <div>
        <h1>Hello! Colors.</h1>
        { !this.isLogin && <button onClick={this.login}>Login</button> }
        { this.isLogin && <p>{this.state.user.email}</p> }
      </div>
    )
  }

  componentDidMount() {
    firebase.auth().onAuthStateChanged((user) => {
      this.setState({ user })
    })
  }

  get isLogin() {
    return this.state.user && this.state.user.uid
  }

  login() {
    const provider = new firebase.auth.GoogleAuthProvider()
    firebase.auth().signInWithRedirect(provider)
  }
}

export default App;

これを ReactDOM.renderレンダリングしてあげればいいんだなぁ。 firebaseのAPIを呼び出すときに毎回認証が必要なので、firebase-config.jsのように書き出しておく。

import firebase from 'firebase/app'
import 'firebase/auth'

const config = {
  apiKey: "APIKEY",
  authDomain: "Domain",
  databaseURL: "databaseURL",
  projectId: "projectID",
  storageBucket: "bucket",
  messagingSenderId: "senderid1234",
  appId: "1:hogehoge:web:1234"
}

firebase.initializeApp(config)

export default firebase

ちなみにFirebaseのConfigのApiKeyはJSファイルに書き出されてしまう。これに対する答えは以下を見れば良さそうだ。

stackoverflow.com

スマブラが発売されたけどホームランコンテストがなかったのでちょっと作ってみる

はじめに

みんなのウェディングのエンジニア@kazumalabです。 この記事はくふうカンパニーアドベントカレンダーのなんと10日目になります!

記事のお題をGraphQLにしてたのですが、タイミングがスマブラ発売と被ってしまいゲームのやりすぎで記事を書けそうありません。 (ほんとミス!)

ですのでお題をチェンジして書こうと思います。全くWeb関係からは離れます!←

スマッシュブラザーズSP

先ほども言いましたが、スマブラが発売されました。 みにいくだけ...と思ってふらっとヤマダ電機に行ったら PayPayで買えば20%Off、運良ければ全額返ってくるという「PayPayキャンペーン」をやっていました。

ええ、その誘惑に負けたわけですね。 ついつい買ってしまいました。

さらにアドカレ当日なのについついやってしまいました。それが以下の写真です。

今年の夏ぐらいにプロジェクターをかったのでその大画面でやるスマブラは格別でした。(そんなことは置いといて...)

今回のアドカレネタ

このスマブラ...ホームランコンテストがない!3DSWiiUまではあったのに... え、じゃあ作ってみてもいいんじゃない?それをアドカレのネタにしちゃおう!というのが今回です。 まぁ結論からいうとホームランを撃ち抜くところまではいかなかったです...。けどダメージを受ければ受けるほどぶっ飛び率が上がっていくところはできたはずです!

そういえば以前に僕は格ゲーを作っていたのを覚えてますでしょうか...はるか昔の記憶です。

これは3年前だったんですね。もっと最近だと思ってました。 時間もないのでどんどん作っていきましょー!

キャラクターを配置

右側がプレイヤ、左側がサンドバックくんです。 サンドバックはもう面倒なのでCylinderそのままです。笑

Playerは有料アセットを使っています。

プレイヤを動かす

今回は簡易的に作るのでCharactorControllerを使ってPlayer.csを実装します。 さらにPlayerのタグをつけておきます。(のちに使う)

private CharacterController charactorController;

void Start () {
    charactorController = this.GetComponent<CharacterController> ();
}

void Update () {
    direction = (Vector3.right * Input.GetAxis ("Horizontal") + Vector3.forward * Input.GetAxis ("Vertical")) * moveSpeed;
    Move (direction);
}

void Move (Vector3 v) {
    charactorController.Move (v);
    if (v.magnitude > 0.01f) { // 向いている方向に向かせる
        transform.rotation = Quaternion.LookRotation (v);
    }
}

とりあえずこんな感じでセットします。 実際にはanimatorで歩くモーションを実装していますが今回は省いています。

あ、カメラはいい感じに(追従する感じなどなど)設定してくださいね!

攻撃する

さて、次はキモとなる攻撃部分を作っていきます。 スマブラではいくつか組み合わせ技がありますが、まずはパンチ一つに絞って実装します。(しかし複数使えるようにはしておく)

今回は与えるダメージ(吹っ飛ばし力)を計算するために必要なデータを準備します。

サンドバックくんのWeight50に設定し、 プレイヤのパンチの威力は以下の図のようになりました。

DMG BKB KBG
5.0 30 100

BKBやKBGについては

www30.atwiki.jp

上記サイトを参考にし、算出についてはスマッシュブラザーズ for WiiUの情報を参考にしました。

Duck Hunt - Kurogane Hammer

ちなみにダックハントのジャブと同じです。

以下のように先ほど動かすために作ったPlayer.csに追記します。

public Dictionary<string, Dictionary<string, float>> actions;
public enum Actions {
    panch
};
public Actions action;
public bool enableHit = false;

private void Start () {
    actions = new Dictionary<string, Dictionary<string, float>>();
    Dictionary<string, float> actionInfo = new Dictionary<string, float>();
    actionInfo.Add("DMG", 5.0f);
    actionInfo.Add("BKB", 30f);
    actionInfo.Add("KBG", 100f);
    actions.Add(Actions.punch.ToString(), actionInfo);
}

private void Update () {
    Attack();
}

public void Attack () {
    if (Input.GetMouseButton (0)) {
        animator.SetInteger ("Attack", 4);
        action = Actions.punch;
        StartCoroutine (AttackTime ());
    } else {
        animator.SetInteger ("Attack", 0); // Attack0にしないと無限に攻撃が動く
    }
}

private IEnumerator AttackTime () { // すこし時間を開けないと攻撃があたり判定されない
    enableHit = true;
    yield return new WaitForSeconds (0.5f);
    enableHit = false;
}

public float getDamage () {
    return actions[action.ToString()]["DMG"];
}

public float getKBG () {
    return actions[action.ToString()]["KBG"];
}

public float getBKB() {
    return actions[action.ToString()]["BKB"];
}

actionsのkeyには攻撃名、valueには与えるダメージが入るようになっています。 Enumで定義したActions一覧からactionsを作ります。

今回はアニメーションをSetIntegerで設定しました。 enableHittrueの時だけ相手に攻撃が当たるようにしています。

action = Actions.punch;

ここでは何の攻撃をしたかを残すようにしています。 これでクリックをすると攻撃アニメーションになり、攻撃可能時間が0.5秒だけ有効になります。(ちょっと微妙仕様)

攻撃を与える、受ける

さてついにサンドバックくんが攻撃を受けるようにします。 まずは攻撃を受ける部分をEnemy.csを作り書いていきます。これはサンドバックくんにコンポーネントとして貼り付けます。

private Rigidbody rb;
private float weight = 50f;

private void Start() {
    rb = GetComponent<Rigidbody>();
}

public void ReceiveDamage(Player target) {
    if (isHit) {
        isHit = false;
        hitpoint += target.getDamage();
        if (rb) {
            // ここのVector.up本当は技の角度とユーザーのスティックから変更するべき
            rb.AddForce((target.transform.forward + Vector3.up) * KnockBack(target));
        }
    }
}

private float KnockBack(Player target) {
    return ((hitpoint * (0.1f + target.getDamage() * 0.05f) * 200 / (weight + 100) * 1.4f + 18) * target.getKBG() * 0.01f + target.getBKB());
}

吹っ飛ばし力を計算する式が以下のサイトに載っていたので引用します。

www30.atwiki.jp

KB = [{敵% * (0.1 + ダメージ * 0.05) * 200 / (敵重量 + 100) * 1.4 + 18} * KBG * 0.01 + BKB] * 補正値

これが先ほどEnemy.csに記述したKnockBack関数になります。

次に攻撃した時に当たった判定をする部分を実装します。

まずは攻撃する側(プレイヤ)の手の部分にColliderをつけます。この時isTriggerにチェックを入れます。Rigidbodyはなくても良さそうです。 スクリプトを書き、手の部分にアタッチします。上の画像でいうとCollisionController.csになります。

public class CollisionController : MonoBehaviour {

    private Player player;
    private GameObject particle; // これは当たった時に発火するParticleなのでお好み
    private Enemy hitEnemy;

    // Use this for initialization
    void Start() {
        player = GameObject.FindGameObjectWithTag("Player").GetComponent<Player>();
    }

    // Update is called once per frame
    void OnTriggerStay (Collider other) {
        if (other.tag == "Enemy") {
            hitEnemy = other.GetComponent<Enemy> ();
            if (player.enableHit && hitEnemy) {
                if (particle == null) {
                    particle = (GameObject)Instantiate (player.hit_particles [0], this.transform.position, player.hit_particles [0].transform.rotation);
                    other.GetComponent<Enemy>().isHit = true;
                    other.GetComponent<Enemy>().ReceiveDamage(player);
                }
            } else {
                particle = null;
                hitEnemy = null;
            }
        }
    }
}

あとは最後にUIをごにょっとすると...

ちょっとスマブラっぽくなりました!

まとめ

今回はノックバックのみの実装になってしまいましたが、継続して開発して「タメ技」とか「バットを使ったタメ技」などを入れてみたいですね。 なかなかスマブラを作るのは厳しいのであれは7000円でも安いと思う...

みんな、、、スマブラ休暇とろうな!(嘘です!!!!!!!!!!!!!!!!!笑)

今回書いたコードは本当はInterfaceを作ってベースとなるクラスを用意して、Enemy、Playerに継承する形にしていました。 ただ、多分そこまで解説できなさそうだったので、冗長ですが、別ものとしてブログ用に実装しました。ホームランコンテスト自分が攻撃を受けることないですし!

明日は @tatsuo48さんのAWSの新サービスについて何か書いてくれるみたいです! お楽しみに!

Railsにおけるメモ化についてちょっと覗き見してみる

こんにちは! みんなのウェディングのエンジニア@kazumalabです。 この記事はくふうカンパニーアドベントカレンダーの3日目になります。

Rubyにおけるメモ化とは

||=を利用して、オブジェクトのキャッシュを行うことができます。 例えば、

def say
  "hello"
end

この場合sayメソッドを呼ぶ場合、振る舞いは同じなのに毎回違うStringクラスのインスタンスが生成されてしまいます。 確認してみます。

irb(main):010:0> say.__id__
=> 70107590117540
irb(main):011:0> say.__id__
=> 70107599432980

どうでしょうか。それではメモ化してみます。

def say
  @text ||= "hello"
end

このメモ化の流れの振る舞い以下のようになっています。

  • 左辺の@textを比較してnot nilだった場合その値を返す
  • @textがnilだった場合は"hello"を@textに代入して@textの値を返す

つまりは@textnilの状態でsayメソッドが呼ばれると@textに"hello"が代入されて、それ以降その@textに入っているオブジェクトを呼び続けるということになります。

irb(main):015:0> say.__id__
=> 70107599401820
irb(main):016:0> say.__id__
=> 70107599401820
irb(main):017:0> say.__id__
=> 70107599401820

何度呼び出しても同じオブジェクトのIDが返ってくることがわかります。

Railsでメモ化を使うケース

まずはメモ化をどういったところで使うのかを考えてみます。

  • Formオブジェクトのgetter
  • Decoratorパターンでメール送信をするときなど
  • APIを利用するとき

いろんなユースケースがあると思います。 さて、Railsでメモ化するとどれぐらいいい感じになるんでしょうか。

Attributesの呼ばれる回数

今回のケースとして、Formオブジェクトで利用する場合を例題にあげてみます。 BookモデルとAuthorモデルがあり、それを同時に生成するFormオブジェクトを以下のように記述してみます。

class BookForm
  include ActiveModel::Model

  attr_writer :name
  attr_accessor :title

  validates :title, :name, presence: true

  def save
    return unless valid?

    ActiveRecord::Base.transaction do
      book.save!
      author.save!
    end
  end

  def name
    @name ||= "NoName"
  end

  private

    def book
      @book ||= Book.new(title: title)
    end

    def author
      @author ||= book.authors.build(name: name)
    end
end

作者名を入力しない場合はNoNameを入れる仕様にしています。

流れ

  • /books/newにアクセス
  • /booksにデータをPOST
  • /books/:idにリダイレクト

/books/newにアクセス

さて、この作者の名前を返すnameメソッドにbinding.pryを仕組み、動きをみてみます。 まず止まったところは/books/newにアクセスしたときです。 /books/newではform_withを利用した以下のようなフォームを設置しています。

= form_with model: @book_form, url: books_path, method: :post, local: true do |f|
  - if f.object.errors.present?
    - f.object.errors.full_messages.each do |message|
      %p= message
  = f.label :title
  = f.text_field :title

  = f.label :name
  = f.text_field :name, value: @book.name
  = f.submit

text_fieldメソッドの中でAttributesが展開されているので、ここでまずは一回呼ばれました。この時点で@nameの中身はnilなのでメモ化されることになります。 exitでpryを抜けると/books/newが表示されました。

しかし、ここで一旦このオブジェクトの寿命は以上になります。

/booksにデータをPOSTする

さて、データを入力して、POSTをします。 Controllerでパラメータを受け取り、BookFormインスタンスを生成します。 その後保存するためのsaveメソッドを呼びますが、その中でvalidationが走ります。

def save
  return unless valid? # here

  ActiveRecord::Base.transaction do
    book.save!
    author.save!
  end
end

まずはそこで呼ばれました。まぁそうですね。nilだった場合はここでNoNameが代入されることになりますが、今回の検証で初めてわかったのですが、空文字だった場合はNoNameは代入されないみたいです。(仕様?)

次に呼ばれたのが、authorsaveするときに呼ばれました。

def author
  @author ||= book.authors.build(name: name)
end

空文字だった場合はメモ化が使えないのは以外でした。 このオブジェクトはバリデーションに失敗した時にはrender :newを呼ぶため、/books/newにアクセスしたときと同じ回数がプラスで呼ばれることになりそうです。今回の場合、validationに失敗した場合も、しなかった場合も3回呼ばれました。

最後に

(時間が間に合わなかったー) 今回の検証方法ではあまりメモ化の良さみたいなところがわかりにくかったと思います。 例えば

def book
  @book ||= Book.find_by(title: title)
end

みたいな感じでメモ化するとViewで何度も呼ばれてもDBへの問い合わせが一回で済みますよね。 そういったようにもう少し効果のわかりやすいのにして計測すればよかったですね。

明日のアドカレは!

@tastuoさんのAWS Lambda Layersを使ってみた!(Python)です!