くらしのマーケット開発ブログ

くらしのマーケットを開発する、みんなのマーケットによる技術ブログ

スタートアップの社内ネットワーク構築アンチパターン

こんにちは。みんなのマーケットでCTOをしております戸澤です。

この記事でわかること

スタートアップは、組織の急拡大のため年単位でオフィスの移転をすることがよくあります。 弊社も移転を繰り返し、その度に新しいオフィスの社内ネットワーク構築を行ってきました。

今回は、これまでの社内ネットワーク構築の経験をもとに、5,30,70名のそれぞれの規模のときに、どんな社内ネットワーク構成にしたかを紹介します。 また最後に、それらの経験から得られたアンチパターンをまとめますので、参考になればと思います。

f:id:curama-tech:20180809205438p:plain:w300

社内ネットワーク構築の歴史

弊社は2011年に創業したスタートアップで、東京と宮崎にオフィスがあります。東京オフィスは、これまでに7回の移転をしています。 このうち、最初の4回はシェアオフィスだったため、社内ネットワークの構築と管理は同居している他の会社の方がやってくれましたが、 5回目からは、自社のみのオフィスとなったため、自分たちで構築と管理をするようになりました。 5,6,7回目のそれぞれの移転で、人数が〜5名, 〜30名, 〜70名規模のオフィスとなったため、この組織規模別に社内ネットワークの構成を紹介していきます。

また、弊ブログの「Amazon Connect使ってみた!」の なぜ電話システム(IVR)が必要なのかでも触れていますが、 弊社には電話を多く使う業務があり、その電話はコンサル電話と呼ばれるブラウザ上から電話できるシステムを使って行われます。 tech.curama.jp

この電話の通信はWi-Fiを通して行われ、音声が途切れたり、相手の声が聞こえないなどのトラブルを何度も経験してきました。

電話というジッターやレイテンシ等の条件がある中で、持ち運びやすさのために有線LANの接続ではなくWi-Fi接続で安定させるための取り組みを行ってきましたので、その経験も参考になればと思います。

〜5名

当時、マンションの1LDK一室をオフィスにしていました。 この広さ、人数であれば、一般家庭でのネット利用と変わらず、家庭向けの機器や回線を選択することでコストも抑えられます。

なので、

  • ルーター(Wi-FiのAP付き): NECの家庭用のもの(Atermシリーズ)
  • 回線: フレッツ(マンションタイプ&最大1Gbps) + 個人向けISP(月800円ほど)

以上の構成で問題なく、ネットが利用できました。

〜30名

30名となると、広さはマンションの一室レベルではなくオフィスレベルになり、クライアント数も多くなるので、家庭用の機器では対応できません。 そのため、ルーターとアクセスポイント(AP)を業務用の機種から新しく導入しました。

ルータの選定は、他の方が書かれた記事を参考にしながら、YAMAHAで規模にあう機種を選びました。 YAMAHAのルーターを初めて導入するため、こちらの本も参考に構築を進めました。

ノンエンジニアのための社内ネットワーク施工・構築ガイド 20~40人規模のLAN環境を自分で作ろう (Do IT Yourself)

ノンエンジニアのための社内ネットワーク施工・構築ガイド 20~40人規模のLAN環境を自分で作ろう (Do IT Yourself)

また、回線もクライアント数の増加に対応できるよう、フレッツより大きい帯域幅が謳われているものを用意しました。

  • ルーター: YAHAMA RTX810
  • AP: Buffaloの業務用AP
  • 回線: フレッツのシェアドアクセス方式を利用した会社のもの

以上の構成で問題なく、ネットが利用できました。

〜70名(現在)

30名のオフィスでは、回線にフレッツより帯域幅の大きいシェアドアクセス方式のものを利用していました。 新オフィスでも継続利用を検討しましたが、移転工事にあたりフレッツよりも開通に時間を要するという説明を受け、 早くの移転を予定していたためそれに間に合うように、比較的開通までの時間が短いフレッツを利用することにしました。

当初、フレッツでは厳しいと考えていましたが、問題なく使えることがわかり現在も継続して使用しています。 NTTの収容局の混雑具合にもよりますが、70名規模でもフレッツで問題なく通信できていることから、〜30名でもファミリータイプであれば問題なく使えたと思っています。

ルーターはRTX810だとNATディスクリプターが近々枯渇することが予想できたので、より多くのNATディスクリプターをサポートしている上位機種のRTX1210を導入しました。

また、APもBuffalo1台のみでは対応できるクライアント数が厳しいので、今後YAMAHAでネットワーク機器を揃える方針でYAMAHAのAPを1台追加し、BuffaloとYAMAHAを併用しました。

  • ルーター: YAMAHA RTX1210
  • AP: Buffaloの業務用AP x1 + YAMAHAのAP x1
  • 回線: フレッツ(ファミリータイプ&最大1Gbps) + 個人用のISP

これでしばらく問題なく使えていたのですが、Windowsアップデートの日に回線が重くなり、パケットロスが多く発生しました。 弊社のWindows端末は数台だけで、社内のWindowsアップデートではなく、そのISPの利用者全体のアップデートに起因しているようでした。

ISP側もWindowsアップデートの日は重くなることを把握していて、毎月のWindowsアップデート日に発生する可能性があり業務に支障がでることが予想されるため、個人用ISPからIIJの法人向けISPに切り替えました。 IIJのISPはSLAがあり、今のところ、Windowsアップデート日やそれ以外でも重くなることは経験していません。

  • ルーター: YAMAHA RTX1210
  • AP: Buffaloの業務用AP x1 + YAMAHAのAP x1
  • 回線: フレッツ(ファミリータイプ&最大1Gbps) + IIJの法人向けISP

この構成で利用していきましたが、人数が60名くらいから稀にコンサル電話の音声が途切れるという問題が発生し始めます。 トラフィックを監視していると途切れが発生する直前に、大容量の通信(ISOファイルのダウンロードなど)が走ると、途切れが発生することがわかり、ルータとAPででQOSと端末ごとの速度制限を導入しました。 速度制限は250Mbpsといったかなり高速の通信をある時間以上すると、通信速度を不便にならない範囲で落とす仕組みです。これで途切れは減ったものの、まだ度々起きる状態でした。

電話トラブルの厄介なところは、原因がネット環境以外にも考えられることです。 ヘッドセットの調子、ブラウザ、電話システムのバグ、相手の通信状況など場合によっては、これらが原因のこともありました。

有線接続の場合は途切れが少ないとの声もあり、社内ネットワーク構築の会社の意見を聞いたところ、 現在の2台のAPでは台数が足りなく、メーカーも別でAP間の連携が取れないため端末分散ができないため、APで通信が詰まっている可能性があるという話になりました。 検証用のAPを導入し試験運用してみたところ、途切れがなくなったため、APの増設工事を依頼しました。

  • ルーター: YAMAHA RTX1210
  • AP: ArubaのAP x4
  • 回線: フレッツ(ファミリータイプ&最大1Gbps) + IIJの法人向けISP

現在は、この構成でネットを利用しています。 電話の途切れはなく、APのメーカーも統一されたことで管理が楽になりました。

これからこうしたい

クライアント数が増えること、トラブル時の損失のインパクトも大きいことを考えると、フレッツのベストエフォートのプランではなく、帯域保証のプランに変えるつもりです。

また、当初からの流れで私がメインとなって社内ネットワークの管理をしてきましたが、今後の規模を考えると次回からはエンジニアにメインの管理をお願いしたいと思っています。

アンチパターン

さて、社内ネットワーク構築の歴史をもとに、アンチパターンをまとめていきます。

機器間の連携が取れないAPを導入する

バラバラのメーカーのAPを導入すると、設定を変える際にmaster機で設定したものをslave機に自動反映することができず、個々に設定することになり管理がつらいです。 また、自動チャンネル変更もAP間で連携が取れなく、双方で永遠とチャンネル変更を繰り返してWi-Fiが何度も途切れることがありました。

現在は同じメーカーのAPを導入していて連携が取れるため、1台のAPにクライアントが集中しないように、AP間での端末分散もできるようになりました。

最初に複数台購入するときはもちろん揃えるとして、 途中でメーカーを変えたくなった場合でも、古いメーカーは使わずに、すべてのAPを新しく導入するメーカーで揃えることをおすすめします。

ジッターやレイテンシが通信要件に入っているのにWi-Fiを使う

専門の会社にAPの増設工事をしてもらい、Wi-Fiでの電話利用ができていますが、今後クライアント数が多くなった場合はどうなるかわかりません。

無線と有線は、トレードオフなところがあるので状況によるとは思いますが、トラブル時の対応工数を考えると有線LANを使うことをおすすめします。

また、ジッターやレイテンシ、パケットロスなどが通信要件に入っている場合、専門の会社に相談して設計や機種選定、キャパシティプランニングを行うことをおすすめします。

SLAのない個人向けISPで頑張る

問題なく使えていたという理由で個人用ISPを継続利用しましたが、ある日回線が遅くなる現象が起きました。

急ぎで導入したい場合は個人ISPの方が早いと思いますが、問題ないからといってそのまま継続するのではなく、 トラブル時の業務や売上へのインパクトを考えて、SLAや帯域保証があるISPに後に変えることをおすすめします。

今の人数だけに対応できる機種を導入する

30名から70名への移転する際、ルーターの買い替えをしました。 数年以内のオフィスの移転が見えているなら、最初から上位機種を購入した方が良かったと思います。

人数に対してオーバースペックにはなりますが、耐用年数は数年程度ではないので、今後の増員を見越して長く使えるものを導入することをおすすめします。

自分ですべて管理できると思う

トラブルが起きた際にネットワークに強いエンジニアの方であれば、問題ないのだとおもいますが、そうでない場合は対応工数が大きいです。 自分でやることで勉強になるという利点はあるのですが、特に電話の途切れトラブルでAPに起因する部分は早い段階で専門業者に相談するべきでした。

最近では、すべてマネージドでやってくれるサービスもあるようなので、社内メンバーがプロダクトに集中して、マネージドできるものはお願いするという選択は大いにありだと思います。

最後に

今回は組織の人数別に事例を紹介しましたが、実際は人数よりも接続するクライアントの数や利用状況への依存が大きいので、考慮した上で適用する必要があります。

最後に、みんなのマーケットに興味がありましたら、会社の雰囲気や事業を知っていただくための、ざっくばらんな面談もやってますので、ぜひ連絡ください。

次回は、Backendエンジニアのトゥエンさんがお送りします。

Swift: Memory leaks、 豊かな人々も泣く!

こんにちは、エンジニアのDuyです。

今日はSwiftのメモリリークについて、話そうと思います。

f:id:curama-tech:20180802152853p:plain:w262:h263

最近、技術が発展しているため、モバイルデバイスの容量も大きくなっているんですが、メモリを管理しないと、アプリが遅くなって、アプリの容量が大きすぎることによって、アプリがcrashする可能性もあります。お金持ちなのにお金を無駄遣いしたら、ある時点でお金がなくなってしまう。お金がなくなったら、豊かな人々も泣く!

そのため、メモリを管理するのが大切です。

つまり、メモリリークとは?

メモリリークはプログラムが確保したメモリの一部、または全部を解放するのを忘れ、確保したままになってしまうことを言う。そのメモリは参照されないので、開放できないし、利用もできない。

リークはいつ?どこから?

アプリを開発するとき、サードパーティのライブラリを使ったら、リークが発生する可能性があるし、CALayerやUILabelのようにAppleによって作成されたクラスからリークが発生する可能性もあります。さらに、デベロッパーのコードはリークがよく発生します。(残念なんですけど、それは本当のことです!)

ある朝、目を覚まして、綺麗な空を見ます。あなたは出社して、自分の席に座って、前日の困っていたことを突然スムーズに解決できますが、アプリを何回かテストしているうちに、だんだんと遅くなってしまいます。あなたがリークを作成しました。(実は寂しくてもうれしくても、リークを作成する可能性があります。)

どうやって、リークを避けることができるでしょうか? リークが発生したら、どうやって、リークを解決できるでしょう?

リークを避ける、あるいはリークを解決できるようにするため、Swiftのリークの原因をさがしましょう!

強参照サイクル (Strong Reference Cycles)

強参照は、参照が存在する限り参照が指すクラスインスタンスを割り当て解除できないことを意味します。下記の例を一緒に見ましょう!

(*) StaffとDeviceの例:

class Staff {
    let name: String
    init(name: String) {
    self.name = name 
    }
    var device: Device?
    
    deinit { 
        print("Staff \(name) is being deinitialized") 
    }
}

class Device {
    let name: String
    init(name: String) { 
        self.name = name 
    }
    var staff: Staff?
    deinit { 
        print("Device \(name) is being deinitialized") 
    }
}

Staffはnamedeviceという2つプロパティがあります。Staffは必ずdeviceがあるとは限らないので、deviceはオプショナルです。 類似、Deviceは2つプロパティがあります。nameとstaffです。

今、変数を定義します。下記の変数のイニシャル価はnilです。

var staff: Staff?
var device: Device?

次に、StaffとDeviceのインスタンスを上の変数にアサインします。

staff = Staff(name: "yamada hanako")
device = Device(name: "macbook")

2つのインスタンスをリンクします。

staff!.device = device
device!.staff = staff

リンクしたあとは、こういう感じになります。

f:id:curama-tech:20180802160844p:plain

変数をnilにします。

staff = nil
device = nil

staff及びdevice変数によって保持されている強参照は無くなりますがが、StaffDeviceのインスタンスはまだ強参照を残ります。それはメモリリークです。

他の例を一緒に見ましょう!

(*) Playerの例:

class Player {
    let name: String
    let age: Int

    lazy var detail: () -> String = {
        return "Name: \(self.name), age: \(self.age) "
    }
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
    
    
    deinit {
        print("Deallocated!")
    }
}

var player: Player?
player = Player(name: "yamada", age: 20)

player?.detail()

player = nil

これから見ると、playerがnilに設定されている時にPlayerインスタンスが割り当て解除されないため、deinitのprintを実行されません。それもメモリリークです。

先程、リークを作りましたが、どうやって解決しますか?

弱参照(Weak Reference)

弱参照は、それが参照するインスタンスを強く保持しない参照です

弱参照は参照するインスタンスを強く保持しない参照であるため、ARCが参照インスタンスを破棄しないようにします。(オフィシャルドキュメント)

(*) StaffとDeviceの例をもう一回見ましょう!

もし、var staff: Staff?の行は weak var staff: Staff?にしたら、下記のスクリプトを実行して、

staff = nil
device = nil

結果は

Staff yamada hanako is being deinitialized
Device macbook is being deinitialized

というのは、StaffとDeviceのインスタンスを割り当て解除されました。リークを解決できました。

weakにはよくThe Delegation Patternを使います。例えば:

    
    open class UITableView : UIScrollView, NSCoding, UIDataSourceTranslating {

        public init(frame: CGRect, style: UITableViewStyle) // must specify style at creation. -initWithFrame: calls this with UITableViewStylePlain

        public init?(coder aDecoder: NSCoder)

        
        open var style: UITableViewStyle { get }

        
        weak open var dataSource: UITableViewDataSource?

        weak open var delegate: UITableViewDelegate?

[...]

または、

@IBOutlet private weak var label: UILabel?

これを見ると、weakはnilをセットできるので、定数にできません。

非所有参照 (Unowned Reference)

弱参照と同様に、非所有参照は、それが参照するインスタンスを強く保持しません。 ただし、弱参照とは異なり、他のインスタンスが同じ存続時間またはより長い存続時間を持つ場合、非所有参照が使用されます。(オフィシャルドキュメント)

例を見ましょう!

class Workspace {
    let name: String
    var channel: Channel?
    init(name: String) {
        self.name = name
    }
    deinit { 
        print("Workspace \(name) is being deinitialized") 
    }
}

class Channel {
    let name: String
    let workspace: Workspace
    init(name: String, workspace: Workspace) {
        self.name = name
        self.workspace = workspace
    }
    deinit { 
        print("Channel #\(number) is being deinitialized") 
    }
}

var workspace: Workspace?

workspace = Workspace(name: "slack")
workspace!.channel = Channel(name: "private", workspace: workspace!)

workspace = nil

Workspaceはchannelがあるかどうか決まっていないがChannelは必ずWorkspaceに属します。

この場合は、WorkspaceChannelのインスタンスは強参照があるので、deinitの関数を実行されません。

しかし、unownedをつかったら?

class Channel {
    let name: String
    unowned let workspace: Workspace // unowned を追加
    init(name: String, workspace: Workspace) {
        self.name = name
        self.workspace = workspace
    }
    deinit { 
        print("Channel #\(number) is being deinitialized") 
    }
}

こういう感じになります。

f:id:curama-tech:20180802160746p:plain

この行

workspace = nil

を実行すれば

Workspace slack is being deinitialized
Channel #private is being deinitialized

理由は、非所有参照のため、workspace変数が保持する強参照を解除すると、Workspaceインスタンスへの強参照はなくなります。メモリリークを解決できました。

キャプチャーリストを定義する(Capture List)

キャプチャリストは、weakまたはunownedキーワードをクラスインスタンス(selfなど)への参照や、値によって初期化された変数(delegate = self.delegate!など)とペアにしたものです。(オフィシャルドキュメント)

Playerの例をもう一回見ましょう!

lazy var detail: () -> String = {
        return "Name: \(self.name), age: \(self.age) "
    }

変更して、

lazy var detail: () -> String = { [unowned self] in // [unowned self] を追加
        return "Name: \(self.name), age: \(self.age) "
    }

もう一回実行したら、結果は

Deallocated!

というのはリークも解決できました。

最後に

私たちテックチームでは「くらしのマーケット」を一緒に作る仲間を募集しています。興味がある方は是非お気軽にご連絡ください (コーポレートサイト https://www.minma.jp/ )。

入社後の新卒研修ってどんなもの?今年はこんなことやりました!

こんにちは、ディレクターのめぐみです!

テクノロジー本部では現在採用に力を入れています。
去年からは新卒採用も始めたのですが、新卒の学生さんが会社選びをするにあたり、公開している情報が少ないってことに気付きました。なので入社したらまずどんなことをするの?そのイメージを持ってもらうべく、エンジニアの新卒研修について紹介します!

エンジニアの新卒研修、こんなことやっています!

テクノロジー本部では、1ヶ月ほどにわたり、みんなのマーケットで使っている技術や仕組みを知ってもらうための研修を行っています。

例として、

  • くらしのマーケットで使っている技術を使ってwebサービスを作る
  • ER図の更新をする

といったことをしています。

具体的には、下の6つのステップを踏んで、システム開発や設計について理解してもらいます。

  • 1.WEB技術の基本である、RESTやHTTPについて理解
  • 2.くらしのマーケットで利用されているmicro-serviceについて理解
  • 3.APIから必要な情報を取得するserverを構築
  • 4.HTTPの基本技術である、GETとPOSTを利用してWEBページとserver間での情報のやり取りを行いwebページに情報を出力するシステムを構築
  • 5.APIから取得した情報をredisを使って一定時間保存し、serverとAPI間の余分な通信を減らす技術を導入
  • 6.serverから非同期通信を利用して情報ごとに更新を行う技術を導入

私たちは「まずは実践してみること」を大切にしているので、実践しながら学べる研修内容になっています。 こうすることで技術面はもちろん、会社のカルチャーを理解してもらうのが狙いです。

新卒研修を受けた社員に感想を聞いてみました!

新卒のtakafumiくんに聞いてみました!

大学では情報系の勉強をしていましたが、今までwebに関することをほとんどやってきませんでした。 ただPythonを使うことには慣れていたので、web系の開発もできるだろうという安易な感覚でいました。しかし、実際はPythonを使うところは出てこず、node.js(typescript)でweb側の開発をすることになってだいぶ苦労しました。 それでも、先輩のエンジニアに0から教えてもらいながら研修を乗り越えることができました。  

とのこと。

くらまのシステムについても理解が深まったそうで、みんなの会話が徐々にわかるようになってきたみたいです。仕事をする上ではまずこれが重要ですね。

大変なことも多かったようですが、「何かを作ることが好き」+「みんなのマーケットで働く人の役に立ちたい」というモチベーションで取り組んでいたようです。

テクノロジー本部だけじゃなくて他部署の研修も参加できるよ!

そもそも、

  • くらしのマーケットってどういうサービスなのか?
  • どんな人がどんな風にサービスを使っているのか?


が理解できていないと開発する上で判断に困ることが出てきます。 なので、サービスについて理解するために、様々なプログラムや研修を用意しています。希望があれば他部署の研修でも参加することができます。

例えば、

  • ユーザーの気持ちを理解するために、予約申し込みから訪問サービスを受けるところまでを体験する
  • 事業者側の利用ルールを理解するために、くらしのマーケットへの出店登録から予約獲得までの流れを体験する
  • 出店している事業者向けの売上アップ講座に参加する

などがあります。売上アップ講座では、サービスを提供している出店者と直接話をすることもできるので、くらしのマーケットへの理解がより深まります 。

最後に

いかがでしたか?入社したら最初にどんなことをやるのかのイメージを持っていただけたでしょうか。
研修内容については受けた社員からフィードバックをもらい、次回の研修に役立てています。研修を受けた後にスムーズに開発に入れるような内容にしていきたいと思っています!

私たちテックチームでは「くらしのマーケット」を一緒に作る仲間を募集しています! 特に2019年新卒採用を積極的にしています。 こちらのサイトよりエントリーしていただいた方には個別説明会やカジュアル面談などを行なっております。
ぜひ応募お待ちしています!

Google Optimiseレポート!どんなデザインの変更ができるのか

はじめに

みんなのマーケットでUI/UXデザイナーをしているミソサクです。

Googleが提供するABテストツール「Google Optimise」を先日初めて使ったので、デザイン的にどんな設定が出来るのかまとめます。「Google Optimiseとは」とか「テストの設定方法」ではなく、見た目をどう変更できるのかに焦点を絞ってます。

テスト設定諸々を飛ばして、いきなりデザインの設定をする「ビジュアルエディタ」に来ます。こんな感じです。

f:id:curama-tech:20180719163539p:plain

「ビジュアルエディタ」で選択できる端末はこちら

  • 標準
  • Nexus7
  • iPad
  • Galaxy S5
  • Nexus5X
  • iPhone5
  • iPhone6
  • iPhone6 Plus
  • レスポンシブ(ベータ版)

※表示を切り替えてもいつまでも表示されない時があるので、再読み込みをするとすぐ切り替わります。

では、早速デザイン的に出来ることをご紹介します。

文字を変える

1. 変更したい要素を選択して「要素を編集」

f:id:curama-tech:20180719163548p:plain

2. 「テキストを編集」か「HTMLを編集」

f:id:curama-tech:20180719163559p:plain

3. 「テキストを編集」の場合、直接テキストを編集し、「完了」

f:id:curama-tech:20180719163852p:plain

3. 「HTMLを編集」の場合、html形式でテキストを編集し、「適用」

f:id:curama-tech:20180719163913p:plain

※ 編集画面を開いて変更する度に保存が必要です、それとは別にページ単位でも保存が必要です。

※ ビジュアルエディタでは変更前との「差分を取る」のではなく、「保存した回数分の変更」が保存されます。

要素を追加する

1. 要素を追加したい箇所の前の(もしくは後の)要素を選択して「要素を編集」

f:id:curama-tech:20180719163548p:plain

2. 「HTMLを挿入」

f:id:curama-tech:20180719163938p:plain

3. HTMLを編集

選択したタグがデフォルトで表示されます

f:id:curama-tech:20180719163952p:plain

4. オプションを選択して「適用」

選択できるオプション

  • 置換・・・そのまんまの意味です
  • 挿入・・・選択した要素内先頭に挿入されます
  • 要素内末尾に追記・・・そのまんまの意味です
  • 次より前・・・とんちみたいだけど単純に選択した要素の前に追加されます
  • 後に挿入・・・そのまんまの意味です

f:id:curama-tech:20180719164005p:plain

要素を削除する

1. 削除したい要素を選択して「要素を編集」

f:id:curama-tech:20180719163548p:plain

2. 「HTMLを編集」

f:id:curama-tech:20180719164028p:plain

3. コードごと削除して「適用」

デフォルトで選択されている「置き換え」以外のオプションを選択すると空のHTML挿入が変更として保存され、要素の削除にはならないのでご注意ください。

f:id:curama-tech:20180719164046p:plain

スタイルを変更する(ページ内の同じ要素全てを変更したい場合)

1. 「< >」アイコンをクリック

f:id:curama-tech:20180719164101p:plain

2 cssを記述して保存

f:id:curama-tech:20180719164119p:plain

スタイルを変更する(ページ内の特定の要素を変更したい場合)

1. 要素を選択してパレットからスタイルを指定して保存

f:id:curama-tech:20180719164133p:plain

パレット内で指定できるスタイル

f:id:curama-tech:20180719164146p:plain

要素の順番を変える

1. 要素をドラッグ&ドロップして保存

なんて簡単。

f:id:curama-tech:20180719164159p:plain

JavaScriptを実行

今回のテストではJavaScriptの変更はなかったので、次回テストする機会があればまたご紹介させていただきます。

さいごに

いかがでしたでしょうか?html、cssの知識がなくてもだいたいのことは出来そうですね!

我々みんなのマーケットテックチームでは「くらしのマーケット」を一緒に作る仲間を募集しています!どんな環境で開発しているかはこちらの記事にまとまっています。興味がある方はぜひ気軽に連絡ください (コーポレートサイト https://www.minma.jp/

ExpressJSフレームワークの一つの単体テストの書き方

はじめに

こんにちは、みんなのマーケットのテックチームのクイです。

前回、ExpressJSフレームワークの紹介という記事で弊社のExpressJSフレームワークを利用している仕方を簡単に紹介しました。今回の続きはExpressJSフレームワークの一つの単体テスト(ユニットテスト)の書き方について紹介します。

単体テスト(ユニットテスト)とは

単体テストはプログラムを構成する部品単位(手続き型プログラミングの関数、クラスのメソッドなど)の動きが正しいかどうか検証するというテストです。

Javascriptにはmochajasmineなどの様々なテスティングフレームワークがあります。今回、mochachaisinonでユニットテストを実装します。

インストール

TypescriptとExpressJSのインストール仕方の参考はこちらです。

npm i --save-dev mocha chai sinon ts-node @types/mocha @types/chai @types/sinon

@types/sinonのバージョンによって使い方が違います。私は@types/sinon@4.3.3を使っています。

環境準備

まずはプロジェクトフォルダにtestフォルダを作成してtestフォルダの中unittestフォルダを作成します。 そして、package.jsonファイルに以下のような設定を追加します。

"scripts": {
    "test": "mocha -r ts-node/register src/**/test.ts",
},

これで、ts-nodeを用いてTypeScriptファイルを直接テストすることができます。

簡単に実装してみましょう

まず、テストしたい関数を作成します。

express/app/logic/example.ts

export function example(): string {
    return "テスト";
}

テストするファイルを宣言します。

express/test/unittest/test_example.ts

import { expect } from "chai";

import { example } from "../../app/logics/example";

describe("Example Test", () => {
    it("Example should return テスト", async () => {
        expect(example()).to.equal("テスト");
    });
});

テストを実行した結果は以下のような出力になります。

$ npm test

> re-express@1.0.0 test /xxx/xxx/xxx/express
> mocha -r ts-node/register test/**/*.ts



  Example Test
    ✓ Example should return テスト


  1 passing (24ms)

上は簡単な例ですが、Dependency Injectionのような複雑なクラスの場合はどうすれば良いでしょう。

SinonでDependency Injection使うクラスをテスト仕方

仮に二つのクラスがあるとします。 VehicleというクラスのstartVehicle()メソッドをテストしたいです。 EngineVehicleに注入されるクラスです。

express/app/logics/engine.ts

export enum EngineStatus {
    Idle = "idle",
    Running = "running"
}

export class Engine {
    private status: EngineStatus = EngineStatus.Idle;

    public getEngineStatus(): EngineStatus {
        return this.status;
    }
}

express/app/logics/vehicle.ts

import { Engine, EngineStatus } from "./engine";

export class Vehicle {
    constructor(
        private engine: Engine
    ) { }

    public getVehicle() {
        if (this.engine.getEngineStatus() === EngineStatus.Idle) {
            return "Vehicle is ready";
        } else {
            return "Vehicle is busy";
        }
    }
}

テストするファイル:

express/test/unittest/test_vehicle.ts

import { expect } from "chai";
import * as sinon from "sinon";

import { Engine, EngineStatus } from "../../app/logics/engine";
import { Vehicle } from "../../app/logics/vehicle";

describe("Test Vehicle", () => {
    let sandbox: sinon.SinonSandbox;
    let vehicle: Vehicle;

    beforeEach(() => {
        sandbox = sinon.createSandbox();
        const engine = sandbox.createStubInstance(Engine);
        engine.setEngineStatus = (status: EngineStatus): EngineStatus => { // setEngineStatusメソッドをモックする。
            return EngineStatus.Running;
        };

        vehicle = new Vehicle(engine);
    });

    afterEach(() => {
        sandbox.restore();
    });

    it("Vehicle should be busy", () => {
        expect(vehicle.getVehicle()).to.equal("Vehicle is busy");
    });
});

テストの結果:

$ npm test

> re-express@1.0.0 test /xxx/xxx/xxx/express
> mocha -r ts-node/register test/**/*.ts



  Test Vehicle
    ✓ Vehicle should be busy


  1 passing (18ms)

テストが失敗するケースを実装してみます。 テストファイルを以下のように修正してください。

it("Vehicle should be busy", async () => {
    expect(vehicle.getVehicle()).to.equal("Vehicle is ready");
});

結果:

$ npm test

> re-express@1.0.0 test /xxx/xxx/xxx/express
> mocha -r ts-node/register test/**/*.ts



  Test Vehicle
    1) Vehicle should be busy


  0 passing (45ms)
  1 failing

  1) Test Vehicle
       Vehicle should be busy:

      AssertionError: expected 'Vehicle is busy' to equal 'Vehicle is ready'
      + expected - actual

      -Vehicle is busy
      +Vehicle is ready
      
      at Object.<anonymous> (test/unittest/test_vehicle.ts:26:41)
      at next (native)
      at /Users/quynv/Documents/express/test/unittest/test_vehicle.ts:7:71
      at __awaiter (test/unittest/test_vehicle.ts:3:12)
      at Context.it (test/unittest/test_vehicle.ts:25:34)



npm ERR! Test failed.  See above for more details.

カバレッジを導入する

まず、必要なパッケージをインストールします。

npm i --save-dev nyc source-map-support

次にpackage.jsonファイルを以下のように修正します。

  "scripts": {
    "test": "nyc mocha"
  },
  ....
  "nyc": {
    "check-coverage": true,
    "extension": [
      ".ts"
    ],
    "require": [
      "ts-node/register",
      "source-map-support/register"
    ],
    "reporter": [
      "lcov",        //どこのまだ実行されないコードが見える
      "text-summary" //結果テキストのもとに見える
    ],
    "exclude": [     //除きたいフォルダ
      "controllers",
      "vendor",
      "bin",
      "test"
    ],
    "report-dir": "./test/coverage/", //テストの結果のHTMLファイルが保存する場所。
    "sourceMap": true,
    "instrument": true
  }

testフォルダの中mocha.optsファイルを作成して以下のような内容を入力します。

/test/mocha.opts

--r ts-node/register
--r source-map-support/register
--full-trace
--bail
test/**/*.ts

テストの結果:

$ npm test

> re-express@1.0.0 test /xxx/xxx/xxx/express
> nyc mocha



  Test Vehicle
    ✓ Vehicle should be busy


  1 passing (18ms)

ERROR: Coverage for lines (75%) does not meet global threshold (90%)

=============================== Coverage summary ===============================
Statements   : 75% ( 9/12 )
Branches     : 75% ( 3/4 )
Functions    : 60% ( 3/5 )
Lines        : 75% ( 9/12 )
================================================================================
npm ERR! Test failed.  See above for more details.

全部のテストが成功しましたが、カバレッジの各値が低いように見えます。

test/coverage/lcov-report/index.htmlファイルを見ましょう f:id:curama-tech:20180711181520p:plain vehicle.tsをクリックして f:id:curama-tech:20180711181540p:plain

上の結果を見るとvehicleファイルに実行していないコードがまだあることがわかります。

つまり、テストケースがまだ足りませんでした。修正しましょう。

engine.tsファイルをテストしたくない場合はファイルの上に/* istanbul ignore file */を挿入して、テストするファイルを以下のように修正します。

describe("Test Vehicle", () => {
    let sandbox: sinon.SinonSandbox;
    let vehicle: Vehicle;
    let engine: Engine;

    beforeEach(() => {
        sandbox = sinon.createSandbox();
        engine = sandbox.createStubInstance(Engine);
        engine.getEngineStatus = (): EngineStatus => {
            return EngineStatus.Running;
        };

        vehicle = new Vehicle(engine);
    });

    afterEach(() => {
        sandbox.restore();
    });

    it("Vehicle should be busy", async () => {
        expect(vehicle.getVehicle()).to.equal("Vehicle is busy");
    });

    it("Vehicle should be ready", async () => {
        engine.getEngineStatus = (): EngineStatus => {
            return EngineStatus.Idle;
        };

        vehicle = new Vehicle(engine);
        expect(vehicle.getVehicle()).to.equal("Vehicle is ready");
    });
});

修正したテストの結果

$ npm test

> re-express@1.0.0 test /xxx/xxx/xxx/express
> nyc mocha



  Test Vehicle
    ✓ Vehicle should be busy
    ✓ Vehicle should be ready


  2 passing (15ms)

=============================== Coverage summary ===============================
Statements   : 100% ( 6/6 )
Branches     : 100% ( 2/2 )
Functions    : 100% ( 2/2 )
Lines        : 100% ( 6/6 )
================================================================================

f:id:curama-tech:20180711181529p:plain f:id:curama-tech:20180711181545p:plain

以上です。

最後に

みんなのマーケット達と一緒に働く仲間を募集しています!。興味がある方はぜひ気軽に連絡ください (コーポレートサイト https://www.minma.jp/ )。