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
リポジトリ
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を実際に使用したテストコードを下記に載せておきます。
リポジトリ
テストコード
// 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で使う条件式をまとめました。
コピペ用。
ソースコード
数値の条件式
角括弧 [ ]
# 等しい [ $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の使い方を下記にまとめる
ソースコード
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]
ソースコード
ソースコードの説明
以下にソースコードの説明を記載していく。
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パターンを実装した。内容をいかにまとめる。
ソースコード
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にするとなおる事がある。