【メモ/未解決】Spring BootでMockMvcのレスポンスボディが空文字になる。

Spring Bootを始めたばかりなのですが、よくわからんことがあったのでメモ。解決していません。私がSpring BootのDIよくわかってないだけだと思う。

あらまし

ServiceをモックせずにControllerのユニットテスト(JUnit4)を実行したらMockMVCのレスポンスボディが空になってしまった。対象のSpring Bootのバージョンは1.5系で、テンプレートエンジンはThymeleafを使用。

テスト対象のController

@Controller
@RequestMapping("/")
public class DemoController {

  @Autowired
  DemoService service;
  
  @GetMapping("hello")
  public String demo (Model model) {
    model.addAttribute("message", service.getMessage());
    return "hello";
  }

}

Service

@Service
public class DemoService {
  public String getMessage() {
    return "message.";
  }
}

失敗するテスト

@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoControllerTests {

  @Autowired
  private DemoController controller;

  private MockMvc mockMvc;

  @Before
  public void setup() {
     mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
  }

  @Test
  public void helloにアクセスする() throws Exception {
    mockMvc.perform(get("/hello"))
      .andExpect(status().isOk()) // ここは通る
      .andExpect(content().string(containsString("message"))); // contentが空文字になってしまって通らない
  } // 実際のアプリケーションで /hello にアクセスすると期待通りの挙動になる

}

ViewResolver?が動いてないっぽい?ControllerだけインジェクトしてもViewResolver?までインジェクトできないのかもしれない。
ServiceがMockBeanでいいならGetting Started · Testing the Web Layer通りに書けばいいが、今回はMockを使わずに通しの動作をテストしたい。

回避策として書いて、かつ期待通りには動作したテスト

とりあえずWebApplicationContextをAutowiredでインジェクトしてみたら動いた。

@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoControllerTests {

  @Autowired
  private WebApplicationContext wac;

  private MockMvc mockMvc;

  @Before
  public void setup() {
    mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
  }

  @Test
  public void helloにアクセスする() throws Exception {
    mockMvc.perform(get("/hello"))
      .andExpect(status().isOk())
      .andExpect(content().string(containsString("message"))); // 期待通り通った
  }

}

※動かないパターンのレスポンスボディが空文字になる理由が理解できてないので、このコードはあくまで回避策。

Right-Click to Calendar 3.8.0をリリースしました。

Right-Click to Calendar - Chrome ウェブストア
3.8.0をリリースしました。変更点は以下のとおりです。

2019/04/13:
・フレーム内で選択したテキストが投稿画面に反映されない不具合を修正しました。
・OAuthトークンの有効期限切れが発生した場合に保存した正規表現が消去される不具合を修正しました。
・パネル機能が削除されたため、設定を消去しました。
・正規表現をテストできる機能を追加しました。

パネルモード、便利だったけど随分前にpicture-in-pictureモードになっちゃって廃止されてしまったので削除しました。
フレーム内のテキスト選択に対してイベント投稿ができないのはずっと直したいと思っていたので、直せてよかったです。iframeとframeに対して有効になります。

内部的な変更点としては、使用しているライブラリが軒並み古くてやばいので、選定し直し&バージョンアップを行いました。入れ子のリストは選定理由です。

  • 日付計算用ライブラリ:jQuery.exDate.js -> Moment.js
    • 使いやすくてドキュメントが充実しているのと、定番のライブラリなので。
  • アラート表示用ライブラリ:SweetAlert 1.x -> SweetAlert2
  • jQuery:1.7 -> 3.4.0
  • Datepicker: jQuery UI Datepicker -> Chrome標準のカレンダー(input type=date
    • Chrome拡張だから絶対Chromeで動くし、じゃあinput type=dateのDatepickerでいいじゃんってなったので。ただ、初見の人はどこでカレンダー表示させるかわからないかもしれないですね…(自分がそうだった)。
    • どうでもいいけど、Edgeは日付選択をドラムロール式でやらせるので滅んでほしい。
  • オプションページとイベント設定ページで使ってるBootstrap:バージョン忘れた -> 4

あと、アロー関数を使ったり、変数宣言のvarをletとconstに書き換えたりしました。これだけでもだいぶ見通しがよくなりました。
jQueryでやってることがDOM操作だけなので、jQueryをやめて素のJavaScrtiptに移行するか悩んだのですが、廃止するメリットが特にないことと、getElementByIdとか長くて嫌だと思ったので、バージョンだけ上げました。
正規表現を複数設定して保存する機能がどうしてもほしかったのですが、リファクタリングで疲れて実装する気力が残ってないので、やる気が出たらどうにかしたいですね…。

機能追加はほぼ無いですが、中身をかなり派手にいじったのと、日付計算がmoment.jsに移行したりしてるので、もし動かないとかあったら教えてほしいです。

あと最後に宣伝というか、これがこの記事のメインなのですが、
劇場版『名探偵コナン 紺青の拳(フィスト)』
めちゃくちゃ面白かったのでぜひ見てください。もはやミステリーでもサスペンスでもなんでもなく内容が荒唐無稽で、製作サイドから要求された要素全部ぶち込んでミキサーにかけたのを脳に直接流し込んでくるタイプの映画でとにかく最高でした。ほんと面白かったです。去年の興行収入絶対超えられないと思うけど私はめちゃくちゃ好きな映画です。ぜひ見てください。

Androidでステータスバーに直接入力・閲覧できるメモアプリを作る。

あらまし

ポケモンGOとかIngressとか、メモリを食うゲームをプレイしてる最中にメモアプリを起動させると、ゲームがKillされてしまって不便だなと思いました。特にポケモンGOは、レイドバトルの待ち時間にアプリが落ちると一陣に入れなくなるから厳しい。
Android 7.0以降はステータスバーからテキストを入力して返信できるので(Direct Replyという機能らしい)、ステータスバーに通知を常駐させてDirect Replyで返信すれば、少なくともプレイ中のゲームはKillされなさそうです。

ステータスバーに常駐させる

ステータスバーから返信できる

レスポンスも表示される

画像とコードが多いので残りは続きに書きました。

Continue Reading

Amazonアソシエイトで売上がない場合Amazon Associates Link Builderは使用できない。

このブログのタイトルの元ネタになっているので、モンスーノのロックのAmazonアソシエイトリンクだけはどうしても設置したいのですが、iframeタイプの広告を選んだ場合、ir-jp.amazon-adsystem.comへのアクセスがタイムアウトしてbodyのレンダリングに時間がかかる問題が発生していました。ちなみにir-jp.amazon-adsystem.comの読み込みが遅いのはあまり問題になっていないのか、調べてみてもほとんど記事が見つかりませんでした。
参考:Amazonアソシエイトのリンクがとても重い - 熊茶壜 + 遊戯三昧
ir-jp.amazon-adsystem.comは1px*1pxの画像で読み込まれますが、用途がアクセス解析なので消して良いと思っています。

やってみたこと

Amazon Associates Link Builder – WordPress プラグイン | WordPress.orgでリンクを生成してir-jp.amazon-adsystem.comのimgタグだけ削除すればいいかと思い、導入してみました。

発生した問題

Amazon Associates Link Builderを使用してアソシエイトリンクを生成しようとした場合
You are submitting requests too quickly. Please retry your requests at a slower rate. For more information, see Efficiency Guidelines.で503エラーが発生しました。一回もAPI実行してないのにtoo quicklyが表示されたので変な笑いが出てしまった。

原因

以下の記事に答えが載っていました。ありがたいです。

売上実績がないとProduct Advertising APIが使用できないようです。別にProduct Advertising APIが使いたいわけではなく、ロックの販売ページまで誘導できればいいので、画像とリンクを普通に組み合わせたうえで、ir-jp.amazon-adsystem.comのimgタグを削除してウィジェットに設置しました。

ロック、今見るとマケプレ価格で59800円になってますが、おもちゃとしての出来はスティックのり以下なので、これ買うならスティックのり買ったほうがいいです。

2019/03/19 追記!!!!!!!!!!!!!

モンスーノの第1話がYoutubeで配信されました!!!この記事書いた次の日に!!!!運命!運命を感じる。(ジョーカーさんっぽく)
【公式】獣旋バトル モンスーノ 第1話「ロック!(鍵)」 - YouTube
傲慢かもしれないんですがリンクを張っておきます。すべての鍵にロック一つってかっこいいけどマジで意味わかんないですね。
CV:KENNで繰り出される主人公のハイセンスで容赦ない煽りとイカれた言動をぜひ堪能してほしいです。1話はそんな煽らないけど。

パルが更新されたら通知するLINE BotをJavaでつくる。

パルの更新をチェックするのが不毛なので、パルが更新されたら通知するLINE botを作りました。Feedlyとかだと見逃す。

使用した環境・ライブラリ

Javaを採用した理由は、持ってるマシン(Windows 10 2台(内1台はほぼすっぴん)、Raspberry Pi 3(raspbian stretch)、CentOS7)全部で動くようにしたかったからです。DockerはWindowsマシンの片割れで正常に動作せず、Windows Subsystem for Linuxはすっぴんのほうでダウンロードに失敗して環境構築がめんどくさくなってしまい、全員JDKなら入ってるからこれでいいやと思いました。環境ごとにOracleJDKかOpenJDKかがバラバラなのですが、大したことはしないので問題ないと判断しました。

全環境でコマンド一発で動くようにしたいので、依存するクラスをすべて内包した実行可能jarを作ることにします。

実際のコード

java

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;

import com.linecorp.bot.client.LineMessagingClient;
import com.linecorp.bot.model.PushMessage;
import com.linecorp.bot.model.message.TextMessage;

import com.rometools.rome.feed.synd.SyndEntry;
import com.rometools.rome.feed.synd.SyndFeed;
import com.rometools.rome.io.SyndFeedInput;

public class RSSReaderBot {

    private static final String CHANNEL_ACCCESS_TOKEN = "TOKEN";
    private static final LineMessagingClient CLIENT = LineMessagingClient.builder(CHANNEL_ACCCESS_TOKEN).build();
    private static final String URL = "http://negineesan.hatenablog.com/rss"; // パルのFeed
    private static final SyndFeedInput INPUT = new SyndFeedInput();
    private static final String CHANNEL_ID = "CHANNEL_ID"; // 通知を流したいチャンネルのID

    public static void main(String[] args) throws MalformedURLException, IOException {

        long lastUpdateDate = 0;

        while (true) {
            // java.net.UnknownHostExceptionが発生するのはopenConnection時ではなくStream取得時なのと、
            // サンプルコードがごちゃごちゃする関係上try文には入れてない
            HttpURLConnection urlConnection = (HttpURLConnection) new URL(URL).openConnection();
            urlConnection.setConnectTimeout(10 * 1000);
            urlConnection.setReadTimeout(10 * 1000);

            try (InputStream stream = urlConnection.getInputStream();
                    InputStreamReader reader = new InputStreamReader(stream, StandardCharsets.UTF_8)) {

                SyndFeed feed = INPUT.build(reader);
                SyndEntry entry = feed.getEntries().get(0); // 最新のエントリだけ取得
                long publishedDate = entry.getPublishedDate().getTime();
                if (publishedDate > lastUpdateDate) { // 「現在時刻」-「更新日時」 < 「更新間隔」 でもいい
                    CLIENT.pushMessage(new PushMessage(CHANNEL_ID,
                            new TextMessage("パルが更新されました" + "\n" + entry.getTitle() + "\n" + entry.getLink())));
                    lastUpdateDate = publishedDate;
                }

            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                urlConnection.disconnect();
                try {
                    Thread.sleep(10 * 60 * 1000); // 10分
                } catch (InterruptedException ignore) {
                }
            }
        }
    }
}

今話題の無限ループで10分毎に更新をチェックします。alertじゃないから大丈夫なんじゃないですかね…(震えながら)。httpclientは不要です。

build.gradle

plugins {
    id 'java'
}

version = '0.1-SNAPSHOT'

dependencies {
    // LINE BOT SDK
    implementation 'com.linecorp.bot:line-bot-api-client:2.4.0'
    // ROME
    implementation 'com.rometools:rome:1.12.0'
    implementation 'org.slf4j:slf4j-simple:1.7.25'
    // test
    testImplementation 'junit:junit:4.12'
}

repositories {
    mavenCentral()
}

tasks.withType(JavaCompile) {
    options.encoding = 'UTF-8'
}

jar {
    manifest {
        attributes 'Main-Class':'biz.retrorocket.hai.RSSReaderBot'
    }
    from configurations.compileClasspath.collect { it.isDirectory() ? it : zipTree(it) }
}

Gradle 3.4あたりからcompileじゃなくてimplementation使わないといけないらしいので以下を参考にimplementationで書きました。

LINE SDKとROMEがslf4j-apiに依存していて、実行時にSLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder"でWARNが出てくるので、slf4j-simpleも一緒に指定しています。

あと、ソースコードをUTF-8で書いている場合、build.gradleのオプションでエンコードを指定するのを忘れるとこうなります。

久々に力強い文字化けを見た。

追記(2019/03/29)

ROMEのサンプルの写経だとStreamを閉じてなかったり、例外処理されなくてループで動かすには厳しかったので修正しました。HttpURLConnectionに関しては一応disconnectしました。
参考:HttpURLConnection#disconnectとKeep-Aliveと - CLOVER🍀