NAKKA-Kの技術ブログ

技術に関する知見や考え方などを投稿します。

DockerとNuxtのSSRでConnectionRefusedが発生する問題の原因を解説する

Docker上でAPIサーバーとNuxt(SSR)を動かす環境で、connection refusedが発生したりしなかったりする問題に遭遇しました。 今回はその問題の発生原因について書いていきます。

一言で

今回の問題は直アクセス時nuxt-linkなどでのclientだけで完結する遷移時でasyncDataメソッドの実行場所が違うためです。

どういうこと?

前者のパターン

まずURLをアドレスバーに直打ちしてアクセスします。 この場合、クライアントでは表示すべきHTMLファイルやjsファイルが存在しない状態から処理が始まり、NuxtサーバーではSSRとしてasyncDataメソッドが実行されます。 そのためasyncDataメソッドにAPIアクセス処理があった場合、Dockerとして動くNuxtサーバー上から行われるためコンテナ~コンテナ間通信になります。

後者のパターン

このパターンでは既にクライアントに表示すべきHTMLファイルやjsファイルが存在していてページが表示されている状態とします。 ページが表示された状態からnuxt-link$router.pushを使って画面遷移をした場合、NuxtサーバーではなくクライアントでasyncDataメソッドが実行されます。 そのためasyncDataメソッドにAPIアクセス処理があった場合、クライアントからDockerコンテナに対してアクセスされるのでホスト~コンテナ間通信になります。

何が問題?

ホストからDockerコンテナへアクセスする場合localhost:8080のような形でアクセスできます。

f:id:NAKKA-K:20191010123923p:plain
クライアントからDockerコンテナへアクセス

しかしDockerコンテナ間のアクセスではコンテナの名前を元にドメインを解釈するため、api:8080のようなアドレスが必要になります。

f:id:NAKKA-K:20191010123951p:plain
Dockerコンテナん間のアクセス

そのためasyncDataメソッドでAPIアクセス処理を書いていると、Nuxtサーバー内で実行される時とクライアントで実行される時の2パターンでは実行結果が変わってしまうといった問題が発生してしまいます。

解決策

サーバー上での実行時と、クライアント上での実行時でURLを変えると解決します! サーバー上ではDockerコンテナ間の通信をする為、コンテナ名依存でURLを設定します。 クライアント上ではDockerコンテナの外からアクセスする為、解釈された後のURLを設定します。

nuxt.config.js

  axios: {
    baseURL: process.env.API_URL,
    browserBaseURL: process.env.API_URL_BROWSER
  },

.env

API_URL='http://api:8080/'
API_URL_BROWSER='http://localhost:8080'

まとめ

SSRをする場合は常に「APIアクセスがどこから行われるのか?」、「どういう環境で行われるのか?」、「本当にそのアドレスで正しいのか?」を意識するようにしましょう。 理解できればそんなに難しい話ではないということがわかります。

参考

env設定忘れでDBが動かず、Dockerを初期状態から立ち上げた

Dockerでpostgresを起動。 同じくDockerでRailsを起動しdocker-composeで接続。

ActiveRecord::NoDatabaseError

FATAL: role "myuser" does not exist

docker-compose up -dした時に環境変数をセットするのを忘れていました。 postgresにボリュームを設定していたので、環境変数をセットしていない間違った状態のまま作成されていました。

$ docker-compose down

これではボリュームなどの永続設定のデータは削除されず上手く初期状態から立ち上げることはできません。

完全に再度作成し直す必要がありました。 以下のコマンドで一度全て削除することができます。

$ docker-compose down -v --rmi all

これで永続化されたボリュームもう一度初期から立ち上げることができます。

$ docker-compose up -d
$ docker-compose run server rake db:create

これで解決しました。

独立したGitリポジトリのマージをすると発生するfatal: refusing to merge unrelated historiesの解決

  1. GitHubリポジトリを作る。
  2. ローカルでリポジトリを作る。

その後、マージしないとと思い立ちgit pull origin masterをすると、fatal: refusing to merge unrelated historiesが表示されてしまいました。

そんな時はということで、git merge --allow-unrelated-histories origin/masterを走らせると見事にマージしてくれました。

完全に独立した2つのブランチがマージされている様子が見られるGitログ
完全に独立した2つのブランチがマージされている様子が見られるGitログ

参考

GoModuleを使ったgo getができない問題を解決する

$GOPATH以下ではデフォルトでGO111MODULE=ofになるため依存パッケージがインストールされておらず、明示的にGO111MODULE=on go get -uをする必要があった。

参考

Laravel5.7でAPIコントローラーをメソッドとしてテストする方法

LaravelでAPIやコントローラーをテストする方法を調べると、大体出てくるのはサーバーを立ち上げてルーティングに対してリクエストを飛ばしてテストする方法です。 今回はコントローラーのメソッドをそのままテストする方法について記述していきます。

なぜそうしたのか?

簡単に言うとテストが軽くなるからです。 メソッドを一つテストするのにすぎず、サーバーとしての機能を一切使う必要がありません。 もちろんこのテストが通ったからといって、ルーティングなどその他の部分が正しいことを担保することはできません。 しかし一番重要であるコントローラーのメソッドが通ることは担保できます。 個人開発として素早く開発していきたかったので、最小限のコストでクリティカルな部分を担保したかったのでこの方式でテストすることにしました。

やること

基本的なテストクラスの記述は普通のテストとなんら変わりません。 Laravel(composer)の機能で自動ファイル作成してもらって構いません。

ここでやることは以下です。

  • ログインする
  • 任意のリクエストを送る
  • コントローラーのメソッドを実行する
  • 戻ってきたステータスをテストする

テストコード

今回はBookControllerという本を司るコントローラーに、本の作成依頼をするテストをします。

use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Request;

class UserBookControllerTest extends TestCase
{
    use RefreshDatabase;

    protected $user;

    public function setUp()
    {
        parent::setUp();
        $this->user = factory(User::class)->create();
    }

    public function testStoreBook(){
        $this->actingAs($this->user, 'api');

        $request = new Request(['isbn' => '9784063842760']);
        $response = \App::make(BookController::class)->store($request, $this->user);
        $this->assertEquals(201, $response->status());
    }
}

まずユーザーをfactoryで作成します。 factoryの説明はこちらをご覧ください。

このコントローラーへのアクセスはログインが必要なので$this->actingAsを使ってログインします。 引数にユーザーを渡し、第二引数にapiでログインすることを明示します。

次にリクエストを作成します。 Requestのコンストラクタに連想配列を渡してあげるだけです。

次にApp::makeメソッドを使用してテストしたいコントローラーを指定します。 戻り値にコントローラーが帰ってくるのでテストしたいAPIのメソッドを呼び出します。 今回はstore(Request $request, User $user)をテストします。

するとレスポンスが帰ってきますので、statusメソッドを実行すればステータスコードを見ることができます。 もしレスポンスデータもテストしたい場合は$response->getData()を実行ればデータが連想配列で帰ってきます。

モック化

コントローラーをテストするときに、内部で重い処理や外部への問い合わせをしている場合はモック化したいと思います。 Laravelのモック化については記事の範囲外なので、以下の記事を参考にしてください。

nakka-k.hatenablog.com

まとめ

テストが早いと開発の効率もガンガン上がりますよね。 テストは書きたいけど、遅くなるのは嫌だと言う人は、この方法でテストすればある程度テストの実行時間を落とせるかもしれませんね。 良いテスト生活を!

Laravel5.7のServiceコンテナをモック化してする方法

Laravelに限らずですが外部のAPIを通信したり、重い処理を実行しているの部分があるのでテストの時はモックにしたい、といった場面は多々あると思います。 今回はServiceコンテナをモック化する方法をまとめていきます。

Serviceコンテナが使われているコード

    public function store(Request $request, User $user)
    {
        // ScrapeManagerの生成
        $scrapers = resolve('app.bookInfo.scrapeManager');

        // ScrapeManagerの実行
        $newBook = $scrapers->searchByIsbn((string)$isbn);

        // ......

Serviceコンテナのキーを使ってresolveで生成しています。 この中身をモック化できればいい感じにテストができそうです。

(Managerなんて責務の大きい名前を使ってんじゃねぇ、という意見は却下します)

テスト内でモック化する

    public function setUp()
    {
        // モックを作成
        $mock = \Mockery::mock(ScrapeManager::class)
            ->shouldReceive('searchByIsbn')
            ->andReturn($this->book)
            ->getMock();

        // モックを適用
        $this->app->bind('app.bookInfo.scrapeManager', function() use ($mock) {
            return $mock;
        });
    }

    public function tearDown()
    {
        parent::tearDown();
        \Mockery::close();
    }

実はこれだけです。 Mockery::mockでモック化対象のクラスを指定します。 shouldReceiveで対象メソッドを指定します。 andReturnで対象メソッドの戻り値を設定します。 あとは最後にgetMockを実行してモックのインスタンスを獲得することを忘れないようにしてください。

次に作成したモックをresolveで解決できる形に設定する必要があります。 $this->app->bindを使います。 第一引数にresolve時に使うキーを指定し、第二引数にはモックを返す関数を設定します。

テスト終了後にMockery::close()を実行することを忘れないようにしましょう。

あとはただただテストをするだけです。 そうすれば処理の中でモック化したキーのresolveが使われれば、モックオブジェクトが呼び出されて任意の処理になります。

テストについて

Laravelでコントローラーテストをする場合にはこちらの記事をご覧ください。

nakka-k.hatenablog.com

まとめ

処理の重い部分、特に外部に影響を与える部分はちゃんとモック化しましょう!! 良いテスト生活を!

vimのGoの言語補助プラグインでUnknown functionバグが発生した

vimをupgradeさせた時に唐突にvimがエラーを吐き出し始めました。 エラーは吐き出すもののとりあえず動いてはいました。 ですがvim-goのある部分でエラーが発生しているようでした。

Error on startup: Unknown function: go#config#CodeCompletionEnabled

とりあえずvim-goのissueを探していると今回の問題に関係ありそうなissueを発見しました。

github.com

このissueに書いてあることを要約すると「sheerun/vim-polyglotから使用される言語系プラグインを、vim-polyglotより先に読み込んではいけない。」というものでした。 vim-polyglotを最後に読み込むようにコードを書き換えて再度読み込みすれば解決しました。 私の場合はdein.vimを使っていたのでキャッシュを削除してもう一度プラグインのインストールを走らせる必要がありました。