canvas.getContext('2d')をjestする

概要

canvas.getContext('2d')が使われているファイルをjestする方法を下記に記載します。

下記のようなコードをjest実行時に動かせるようにします。

this.canvas = document.getElementById('gray-scot-model-canvas') as HTMLCanvasElement
const ctx = this.canvas.getContext('2d')

環境

@vue/cli 4.5.9

リポジトリ

github.com

canvasMock.jsの作成

jestを走らせる時に使うcanvas関連のmockを作ります。

今回は、getContextをmock化します。

class ContextMock{
  fillStyle

  fillRect(x, y, cellWidth, cellHeight){ }
}

HTMLCanvasElement.prototype.getContext = () => { 
  return new ContextMock
};

contextオブジェクト生成後にfillStyleとfillRectが使用されるので、定義しています。
fillRectが受け取る引数も検証したい場合はjest.fn()の利用が考えられます。

jest.config.jsの設定

作成したmockファイルをjest.config.jsに設定します。
setupFilesにmockファイルのファイルパスを渡します。

module.exports = {
  preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel',
  moduleNameMapper: {
    '\\.css$': '<rootDir>/tests/styleMock.js'
  },
  setupFiles:['<rootDir>/tests/canvasMock.js']
}

document.body.innerHTMLにhtmlを代入

テストファイルにて、 document.body.innerHTMLにhtmlを代入します。

'<canvas id="gray_scot_model_canvas" width="800" height="500" class="canvas">' +
'</canvas>';

これによりjestでgetElementByIdがコールされた時にcanvasObjectが帰ってくるようになります。

以上で、canvas.getContext('2d')からmockが帰ってきてjest出来るようになります。

Jestを使ってClassのメソッドを一部Mock化する方法

概要

Jestを使ってClassのメソッドを一部Mockにしてテストを回す方法を記載します

結論

prototypeを使い該当メソッドを対象のテストファイルにてjest.fnで更新します

// prototypeを使用してメソッドをmockに更新
Class.prototype.method = jest.fn()

// 戻り値も定義したい場合、mockReturnValueを使用する
Class.prototype.method = jest.fn().mockReturnValue(1)

例えば、MemberクラスのgetNameメソッドをmock化するなら

Member.prototype.getName = jest.fn()

となります。

参考コード

prototypeを実際に使用したテストコードを下記に載せておきます。

リポジトリ

github.com

テストコード

  // outline: Check arguments when run createGrayScotModel
  it('should create gray-scott-model with expected arguments', () => {
    const createMethodMock = jest.fn()
    GrayScotModelFactory.prototype.create = createMethodMock.mockReturnValue({
      materialU: Array.from(new Array(100), () => new Array(100).fill(0)),
      update: () => {}
    })

    const wrapper: any = mount(GrayScotModelSimulator)

    expect(createMethodMock.mock.calls.length).toBe(1);
    expect(createMethodMock.mock.calls[0]).toMatchObject([ 0.022, 0.051, 100, 12 ])
    expect(createMethodMock.mock.results[0]["type"]).toBe("return");
    
    wrapper.vm.createGrayScotModel(0.035, 0.065)

    expect(createMethodMock.mock.calls.length).toBe(2);
    expect(createMethodMock.mock.calls[1]).toMatchObject([ 0.035, 0.065, 100, 12 ])
    expect(createMethodMock.mock.results[1]["type"]).toBe("return");
  })

ShellScriptの条件式まとめ

概要

ShellScriptで使う条件式をまとめました。
コピペ用。

ソースコード

github.com

数値の条件式

角括弧 [ ]

# 等しい
[ $value -eq 1 ];  

# 等しくない
[ $value -ne 2 ];

# ~より上
[ $value -gt 0 ]; 

# 以上
[ $value -ge 1 ];

# 〜より下(未満)
[ $value -lt 2 ];

# 以下
[ $value -le 1 ];

2重丸括弧 (())

# ※存在しないので注意
x (($value = 1));  

# 等しい
(($value == 1)); 

# 等しくない
(($value != 2));

# 〜より上
(($value > 0));

#  以上
(($value >= 1)); 

# 〜より下(未満)  
(($value < 2)); 

# 以下
(($value <= 1)); 

文字列の条件式

角括弧 [ ]

# 等しい(こちらは動く)
[ $value = "abc" ];

# 等しい
[ $value == "abc" ];

# 等しくない
[ $value != "abcd" ]; 

2重角括弧 [[]]

# 〜より上 (ASCII順)
[[ $value > "123" ]];

# 動かないので注意
x [[ $value >= "123" ]];

# 〜より下(未満) (ASCII順)
[[ $value < "bcd" ]]; 

# 動かないので注意
x [[ $value <= "bcd" ]];

Nimのiteratorの使い方

概要

Nimのiteratorの使い方を下記にまとめる

ソースコード

github.com

1行目~3行目

type Animal = object
  name: string
  age: int

Animalを定義。

要素は2つで、nameとageのみ。

5行目~6行目

type AnimalBox = object
  animals: seq[Animal]

AnimalBoxを定義。

要素は1つでAnimal 型のsequenceをanimalsとして所持する。

8行目~13行目

iterator eachName(box: AnimalBox): string = 
  let length = box.animals.len
  var i = 0
  while i < length:
    yield box.animals[i].name
    inc i

eachNameというiteratorをAnimalBoxに取り付ける。

Nimではiteratorを定義する時iteratorと書く。

非常にわかりやすい。

この例では、animalsのnameを1つずつ取り出すiteratorを定義。

15行目~20行目

iterator eachNameAndAge(box: AnimalBox): tuple[a: string, b: int] = 
  let length = box.animals.len
  var i = 0
  while i < length:
    yield ( box.animals[i].name, box.animals[i].age )
    inc i

2つめのiteratorを定義。

今度は、nameとageのtupleを1つずつ取り出す。

22行目~35行目

let dog = Animal(name: "dog", age: 5)
let cat = Animal(name: "cat", age: 4)
let bird = Animal(name: "bird", age: 3)
let animalBox = AnimalBox(animals: @[dog, cat, bird])

echo "animalBox.eachName:"
for name in animalBox.eachName():
  echo name

echo ""

echo "animalBox.eachNameAndAge:"
for pair in animalBox.eachNameAndAge():
  echo pair

AnimalBoxにとりつけた、iteratorを使用する。

可読性が高く使いやすい。

出力結果

animalBox.eachName
dog
cat
bird

animalBox.eachNameAndAge
(a: "dog", b: 5)
(a: "cat", b: 4)
(a: "bird", b: 3)

NimでFactoryパターンを実装する

概要

以前からfactory(又はstrategy)をNimで実装できないかということを、ちょこちょこ調べていた。

今回、実装に成功したので、そのことを以下に記載する。

環境

Nim Compiler Version 1.4.0 [Linux: amd64]

ソースコード

github.com

ソースコードの説明

以下にソースコードの説明を記載していく。

1行目~2行目

type AnimalKind {.pure.} = enum
  DOG, CAT, BIRD

あとでFactoryのcreate部分で使うAnimalKindという名前のenumを定義。

pragmaの一種である{.pure.}をつけることによって必ずAnimalKind.DOGといった形で呼び出さなければならなくなる。

(DOG単体で呼び出そうとするとエラーになる)

o AnimalKind.DOG
x DOG

4行目~6行目

type Animal = ref object of RootObj
  name: string
  kind: AnimalKind

継承元のオブエジェクトを作成する。

プロパティは2つでありnameをstring型, kindをAnimalKind型で定義。

objectだとヒープ領域に作られずに動的にできないのでref objectにしている。

ref objectにしないとFactoryでcreate出来なくなるので注意。

恐らくコンパイルは通っても実行でエラーになるだろう。

8行目

method getKind(animal: Animal): string {.base.} = "ANIMAL"

先程定義したAnimalにBaseとなるmethodを定義する。

今回は分かりやすくgetKindと叩かれると何かしらの文字列が帰ってくるだけというシンプルなmethodにした。

nimには他にもprocやfuncといった関数の定義方法があるが オブジェクト思考で実装する場合は、methodを使う。

(procとfuncは手続き型で使ってfuncは副作用を許さない固い関数を定義出来る)

10行目~17行目

type Dog = ref object of Animal
method getKind(dog: Dog): string = "DOG"

type Cat = ref object of Animal
method getKind(cat: Cat): string = "CAT"

type Bird = ref object of Animal
method getKind(bird: Bird): string = "BIRD"

ref object of XXXで継承する。

Animalを継承したDog、Cat、Birdクラスを定義し、それぞれ別の文字列が帰ってくるgetKindメソッドを定義する。

19行目~24行目

type AnimalFactory = object
func create*(factory: AnimalFactory, kind: AnimalKind): Animal = 
  case kind
    of AnimalKind.DOG: return Dog(name: "xxx", kind: kind)
    of AnimalKind.CAT: return Cat(name: "ooo", kind: kind)
    of AnimalKind.BIRD: return Bird(name: "iii", kind: kind)

今回の目的であるAnimalを作成するAnimalFactoryを実装。

ヒープ領域を使う必要がないのでobjectで定義。

アスタリスク(*)をつけることで外から、この関数を叩けるようになる。

戻り値の型をAnimalにすることにより、Animalを継承しているDog, Cat, Birdが返却できるようになる。

26行目~35行目

let factory = AnimalFactory()

let dog = factory.create(AnimalKind.DOG)
echo dog.getKind()

let cat = factory.create(AnimalKind.CAT)
echo cat.getKind()

let bird = factory.create(AnimalKind.BIRD)
echo bird.getKind()

AnimalFactoryを実際に使用する。

タグとしてAnimalKindをわたしてdog, cat, birdを作成している。

echoで異なる結果が出力されたのなら成功。

実行結果は以下となる。

DOG
CAT
BIRD

Rubyでspecifiactionパターンを実装

概要

Rubyでspecifiactionパターンを実装した。内容をいかにまとめる。

ソースコード

github.com

specificationパターンとは

specificationパターンとは、「複雑な仕様部分を外に切り出す事が出来る」ソフトウェアデザインパターン

オブジェクトに対する「検証」や「選択」といった用途で利用される。

詳しくは、Eric Evans氏とMartin Fowler氏によるspecifiactionパターンの論文があるのでそちらを参照。

「複雑な仕様部分を外に切り出す事が出来る」ってなに?

コードが複雑な業務のルールの影響を受けてif文だらけになることがある。

「複雑な仕様部分を外に切り出す事」っていうのは、その邪魔なif文をメインのコードからどかして責務を分けること。

また、どかすときに別クラスで実装する。

オブジェクト

今回はゴリラオブジェクトを使って実装していく。 ゴリラには、名前と性別と役職がある。

module GoriraMod
  class Gorira
    attr_reader :name
    attr_reader :sex
    attr_reader :position

    def initialize name:, sex:, position:
      @name = name
      @sex = sex
      @position = position
    end
  end
end

仕様クラスの実装

以下が実装した仕様クラスとなる。

require "./const"
require 'date'
module GoriraMod
  class PurchaseBananaSpecifiaction
    def initialize
      # 結果を変えてくないから固定値を入れている
      # 本当は、Date.todayのつもり
      @current_date = Date.new(2020, 2, 05)
    end

    def is_satisfied_by(gorira)
      # - 11月以降なら、全てのゴリラがバナナを購入可能
      return true if @current_date.month >= 11
      # - 性別がメスでありポジションが一般スタッフである
      return true if female_and_common?(gorira)
      # - 性別がオスでありポジションが社長である
      return true if male_and_president?(gorira)

      # 上の仕様に当てはらまなかったゴリラは購入不可
      return false
    end

    private

    def female_and_common? gorira
      gorira.sex == Const::GoriraMod::Sex::FEMALE && gorira.position == Const::GoriraMod::Position::COMMON
    end

    def male_and_president? gorira
      gorira.sex == Const::GoriraMod::Sex::MALE && gorira.position == Const::GoriraMod::Position::PESIDENT
    end
  end
end

クラス名はxxSpecification(xx仕様)という名前で実装。

判定部分のメソッド名はis_satisfied_by(~によって満たされる)という名前で実装。

is_satisfied_byの中にメインコードに出てきてほしくないif文だらけのコードを突っ込んだ。

compositeパターンを利用して、これにandメソッドやorメソッドを実装して仕様同士を組み合わせるようなやり方もある。

仕様クラスの利用

GoriraContainerというゴリラを格納するクラスにて仕様クラスを利用した。

利用されるたびにnewされるので場合によっては、クラスメソッド化してしまったほうがいいかもしれない。

require "./gorira_mod/specification/purchase_banana_specifiaction"
module GoriraMod
  class GoriraContainer
    def initialize goriras
      @goriras = goriras
    end

    # [検証]
    # バナナを購入できるゴリラを保持しているかどうか
    def has_can_purchase_gorira?
      specification = PurchaseBananaSpecifiaction.new()
      @goriras.any?{|a| specification.is_satisfied_by(a) }
    end

    # [選択]
    # バナナを購入できるゴリラのみを抽出する
    def select_can_purchase_banana_gorira
      specification = PurchaseBananaSpecifiaction.new()
      @goriras.select{|s| specification.is_satisfied_by(s) }
    end
  end
end

検証が仕様を利用してbool値を返却するメソッド、選択が何かを抽出するメソッドになる。

選択で使う仕様はSQLクエリになることもある。その場合、リポジトリ層を使うことになる。

実行する

最後にコードをつなぎ合わせて実行する。

require "./gorira_mod/object/gorira"
require "./gorira_mod/object/goria_container"
require "./const"

gorira_1 = 
  GoriraMod::Gorira.new(name: "ごりお", 
            sex: Const::GoriraMod::Sex::MALE, 
            position: Const::GoriraMod::Position::COMMON)

gorira_2 =
  GoriraMod::Gorira.new(name: "ごりみ", 
            sex: Const::GoriraMod::Sex::FEMALE, 
            position: Const::GoriraMod::Position::LEADER)
    
gorira_3 =
  GoriraMod::Gorira.new(name: "ごりすけ", 
          sex: Const::GoriraMod::Sex::MALE, 
          position: Const::GoriraMod::Position::PESIDENT)

goriras = [gorira_1, gorira_2, gorira_3]
container = GoriraMod::GoriraContainer.new(goriras)

if container.has_can_purchase_gorira?
  puts "バナナを購入できたゴリラがいます!"
end

puts "------------------------"

puts "購入できたゴリラを以下に出力"

container.select_can_purchase_banana_gorira().each do |e|
  puts e.name
end

結果

バナナを購入できたゴリラがいます!
------------------------
購入できたゴリラを以下に出力
ごりすけ

Railsのapp以下のファイルで自動読み込みが効かない時

概要

このトラブルは、Service層などの新たな設計をappディレクトリ配下に実装する際によく直面する 自動読み込みが効いていない時に確認する項目を下記に記述する

ActiveSupport::Dependencies.autoload_pathsの確認

自動で読み込んでくれるファイルパスがどうなっているのかは下記のプログラムにて確認できる
rails cを起動して叩けばいい。

ActiveSupport::Dependencies.autoload_paths

app以下のディレクトリなら自動的に全て登録されている。
ここに入っていないのであれば、autoload_pathsが開発者の手によって操作されている可能性が高い。
config/initializersディレクトリなどに関連するコードがないかどうかを調べよう。

.autoload_pathsにいるけど読み込まれない。

恐らく、namespace絡みのトラブルにハマっている。
Railsは無作法者には厳しいのでしっかりと規則を守らないとファイルの自動読み込みは効かなくなる。

[ 自動読み込みが効く例 ]

ファイルパス: app/service/user_service.rb

class UserService
    ...
end

[ 自動読み込みが効く例2 ]

ファイルパス: app/service/service/user_service.rb

module Service
  class UserService  
  ...
  end
end

[ 自動読み込みが効かない例 ]
ファイルパス: app/service/user_service.rb

module Service
  class UserService  
  ...
  end
end

bootsnapのchacheが効いている

挙動がおかしい、何をやっても治らない場合は一度キャッシュ周りを調査する。

boot.rbに以下のようなコードがないかチェック。

require 'bootsnap'
env = ENV['RAILS_ENV'] || "development"
Bootsnap.setup(
  cache_dir:            'tmp/cache',   
  development_mode:     env == 'development',
  load_path_cache:      true,  
  autoload_paths_cache: true,      
  disable_trace:        true, 
  compile_cache_iseq:   true, 
  compile_cache_yaml:   true
)

load_path_cacheやautoload_paths_cacheをfalseにするとなおる事がある。