Spring boot の MVCテストの時の各種sessionのnull pointerを解決する。


開発しているspring boot のアプリケーションにて
MockMvcを使用してmvcのテストをしていた時、
session変数を取得するときにnull pointerで例外をはくのでその際の解決方法をまとめました。

もしかすると見当違いや他の方法もあると思いますので
新しい方法、ベターな方法が分かり次第修正します。記述間違い、認識間違いがあってご指摘等いただければありがたいです。そのときは貴重なお時間いただき感謝します。

書かないこと

・テストのセットアップ (gradle やmvnのセット)
・そもそもsessionを使用した記述や作り方が正しいかどうかは別問題なので(また別の解決があるとおもいますので)その部分には触れません。勉強します。
・認証周りの記述方法は書いておりません。
.with(user(mockuser))にて認証して実行しています。

環境

spring boot 1.3.x
java 1.8

背景

MVCテストを行うとき、コントローラーメソッドを
実行させた後、session系のインスタンスを参照するときにnull pointerになり
テストを行うことができず困っていました。
考えてみると実際にログインして操作するわけではないので
ログインして操作することを前提としているsessionインスタンスがないのは
当たり前なのですが、どのようにしたらいいかわからずこまっていました。
いろいろ調べた結果なんとか進めることができたのでメモ的に記述するのと、
解決するまでかなり時間がかかったので同じ箇所で困っている方がいまして参考になれば幸いです。

この記事で指すMVCテストとは

まず本件で言うMVCテストとのことを一応どのことかを提示します。
いわゆる以下のようにURLを指定してHTTPメソッドを指定して
return値その他各種状態をテストするもののことをさします。

        @Test
        public void getTest() throws Exception {

            ResultActions resultActions =
                    mockMvc.perform(MockMvcRequestBuilders.get("/data/showList")                    
                            .with(user(mockAdminUser)))
                            .andExpect(MockMvcResultMatchers.status().is2xxSuccessful());
        }

本題 3つの問題点

問題は主に3つのnull pointerで困っていました。

  • ① request.getSession.getAttributeで使用するセッションがセットされていないのでnullになる。
  • @Autowiredしている HttpServletRequest をインスタンス化している部分でインスタンスがnullである。
  • ③ session beanで定義した変数が@Autowiredで使用するときにnullである。

解決した方法

①について

この問題はテストで行うURLにアクセスしたときに
ロジック内で以下の記述があったときにnull pointerが発生していました。


    @Autowired
    HttpServletRequest request;

    public void f() {
        //request がnull で .getSession時にnull pointer Exception
        Object o = request.getSession.getAttribute("abc");
    }

この問題はテスト行うときに以下のようにセットすることで
HttpServletRequest をインジェクションできるようになりました。


    HttpServletRequest request;

    request = new MockHttpServletRequest();
    RequestContextHolder.setRequestAttributes(new ServletWebRequest(request));

以上をテスト実行時の@Beforeの箇所などセットアップ時に記述することでテストするメソッド内での呼び出しでインスタンスを使用することができます。
同じプロジェクトにアサインされていたオフショアのメンバーが他のテスト(Mvcテストではない)で使用していたのでそれを参考にさせていただきました。

英語は苦手なのですが、以下のようにあるので
意味的には「現在のスレットでRequestAttributesを束縛する」
的な意味でしょうか

    /**
         * Bind the given RequestAttributes to the current thread,
         * <i>not</i> exposing it as inheritable for child threads.
         * @param attributes the RequestAttributes to expose
         * @see #setRequestAttributes(RequestAttributes, boolean)
         */
        public static void setRequestAttributes(RequestAttributes attributes) {
            setRequestAttributes(attributes, false);
        }

②について

null pointerが発生する箇所は以下のような記述です。

    @Autowired
    HttpServletRequest request;

    public void f() {
        MyClass o = (MyClass)request.getSession.getAttribute("abc");
        // setAttributeで値をセットされていないので
        o.getName();//null pointer exception
    }

このような記述があるとき、requestでセットした変数はこのメソッドに入る前のどこかで
setAttributeされていることが前提なのですが MVC テストではある特定箇所を指定するのでセットされていない場合当然nullになります。

これはMockHttpSession のインスタンスを作成し、
mvc.performするときにセットして解決します。

まず以下のように記述してMockHttpSessin のインスタンスを作成します。
複数のsession変数を指定できるようにメソット化します。


    public static MockHttpSession getMockHttpSession(Map<String, Object> sessions) {
            MockHttpSession mockHttpSession = new MockHttpSession();
            for (Map.Entry<String, Object> session: sessions.entrySet()) {
                mockHttpSession.setAttribute(session.getKey(), session.getValue());
            }
            return mockHttpSession;
        }

引数をマップにしていますので以下のように使います。
MockHttpSessionをmockMvc.perform時に一緒に渡します。


    Map<String,Object> sessionMap =  new LinkedHashMap<String, Object>(){{
                    put("id", 123);
                    put("userName", "taro");
                }};
    MockHttpSession mockSession = 
            getMockHttpSession(sessionMap);


    ResultActions resultActions =
                    mockMvc.perform(MockMvcRequestBuilders.get("/data/showList")                    
                            .session(mockSession)
                            .with(user(mockAdminUser)))
                            .andExpect(MockMvcResultMatchers.status().is2xxSuccessful());

これでテスト時に使用されるメソッド内で、session変数に値がある状態になります。

③について

以下のようにRequestScopeとして指定した変数を設定し
その変数をインジェクションしたメソッド内でnull pointerになりました。


    @Data//lombok使用
    class MySession {
        MyClass me;
    }
   @Bean
   @Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
   public MySession mySession(){
       MySession m = new MySession();
       return m;
   }


    @Autowired
    Mysession mySession


    public void calcService(){
        //null pointer exception
        mySession.getMe()
    }

これも②と同様の理由であたりまえなのですが、②の方法では値をセットできないので方法を探していました。
こちらの記事を参考にして解決しました。
https://stackoverflow.com/questions/2411343/request-scoped-beans-in-spring-testing

以下のクラスを作り
WebApplicationContext.SCOPE_SESSIONを登録する記述をします。


    public class WebContextTestExecutionListener extends AbstractTestExecutionListener {

        @Override
        public void prepareTestInstance(TestContext testContext) {
            if (testContext.getApplicationContext() instanceof GenericApplicationContext) {
                GenericApplicationContext context = (GenericApplicationContext) testContext.getApplicationContext();
                ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
                beanFactory.registerScope(WebApplicationContext.SCOPE_REQUEST,
                        new RequestScope());
                beanFactory.registerScope(WebApplicationContext.SCOPE_SESSION,
                        new SimpleThreadScope());
            }
        }
    }

これをテストクラスにアノテーションします。


    @ActiveProfiles("test")
    @RunWith(SpringJUnit4ClassRunner.class)
    @SpringApplicationConfiguration(classes = TestConfig.class)
    @WebIntegrationTest(randomPort = true)
    @TestExecutionListeners({WebContextTestExecutionListener.class,
            DependencyInjectionTestExecutionListener.class,
            DirtiesContextTestExecutionListener.class})
    @Transactional
    public class MvcTestSample {

これでテスト時にこの変数にセットしておくと
mockMvc.perform時にアクセスした後のメソッド内で
この変数を参照する記述があったときに値がある状態になります。

        @Autowired
        Mysession mySession


        @Before
        public void setUp() throws Exception {
            MyClass me = new MyClass();
            mySession.setMe(me);   
        }

以上でとりあえずこの問題を解決しテストコードの記述をすすめることができました。
テストは絶対必要と思うのですがテストするまでに各種セットアップするまでが大変ですね。

②に関しての解決方法もなにか他の記事を参考にさせていただいたのですが、
礼儀としてリンクするべきなのですが、どこかわからなくなってしまいました。
分かり次第リンクさせていただこうと思います。

以上です。