如何测试JSON路径是否不包含特定元素,或者元素是否存在则为空?

时间:2015-09-04 12:07:21

标签: java unit-testing spring-mvc jsonpath mockmvc

我一直在为一个简单的spring web应用程序编写一些简单的单元测试例程。当我在资源的getter方法上添加@JsonIgnore注释时,生成的json对象不包含相应的json元素。因此,当我的单元测试例程尝试测试它是否为null(这是我的情况的预期行为,我不希望密码在json对象中可用)时,测试例程会遇到异常:

  

java.lang.AssertionError:JSON路径没有值:$ .password,exception:路径没有结果:$ ['password']

这是我编写的单元测试方法,用is(nullValue())方法测试'password'字段:

@Test
public void getUserThatExists() throws Exception {
    User user = new User();
    user.setId(1L);
    user.setUsername("zobayer");
    user.setPassword("123456");

    when(userService.getUserById(1L)).thenReturn(user);

    mockMvc.perform(get("/users/1"))
            .andExpect(jsonPath("$.username", is(user.getUsername())))
            .andExpect(jsonPath("$.password", is(nullValue())))
            .andExpect(jsonPath("$.links[*].href", hasItem(endsWith("/users/1"))))
            .andExpect(status().isOk())
            .andDo(print());
}

我也尝试过使用jsonPath()。exists()获得类似的异常,说明路径不存在。我正在分享更多的代码片段,以便整个情况变得更具可读性。

我正在测试的控制器方法看起来像这样:

@RequestMapping(value="/users/{userId}", method= RequestMethod.GET)
public ResponseEntity<UserResource> getUser(@PathVariable Long userId) {
    logger.info("Request arrived for getUser() with params {}", userId);
    User user = userService.getUserById(userId);
    if(user != null) {
        UserResource userResource = new UserResourceAsm().toResource(user);
        return new ResponseEntity<>(userResource, HttpStatus.OK);
    } else {
        return new ResponseEntity<>(HttpStatus.NOT_FOUND);
    }
}

我使用spring hateos资源汇编程序将实体转换为资源对象,这是我的资源类:

public class UserResource extends ResourceSupport {
    private Long userId;
    private String username;
    private String password;

    public Long getUserId() {
        return userId;
    }

    public void setUserId(Long userId) {
        this.userId = userId;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    @JsonIgnore
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

我理解为什么这会产生异常,同样在某种程度上,测试成功无法找到密码字段。但我想要做的是,运行此测试以确保该字段不存在,或者如果存在,则它包含空值。我怎样才能做到这一点?

堆栈溢出中有类似的帖子: Hamcrest with MockMvc: check that key exists but value may be null

在我的情况下,该字段也可能不存在。

对于记录,这些是我正在使用的测试包的版本:

    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-core</artifactId>
        <version>2.6.1</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-annotations</artifactId>
        <version>2.6.1</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.6.1</version>
    </dependency>
    <dependency>
        <groupId>com.jayway.jsonpath</groupId>
        <artifactId>json-path</artifactId>
        <version>2.0.0</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>com.jayway.jsonpath</groupId>
        <artifactId>json-path-assert</artifactId>
        <version>2.0.0</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-all</artifactId>
        <version>1.10.19</version>
        <scope>test</scope>
    </dependency>

提前致谢。

[编辑] 更准确地说,你必须为一个实体编写测试,你知道某些字段需要为空或空或者甚至不存在,并且你实际上并没有通过代码来查看是否存在一个JsonIgnore添加在属性的顶部。并且您希望您的测试通过,我该怎么做。

请随时告诉我这根本不实用,但仍然很高兴知道。

[编辑] 上面的测试成功使用了以下较旧的json-path依赖项:

    <dependency>
        <groupId>com.jayway.jsonpath</groupId>
        <artifactId>json-path</artifactId>
        <version>0.9.1</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>com.jayway.jsonpath</groupId>
        <artifactId>json-path-assert</artifactId>
        <version>0.9.1</version>
        <scope>test</scope>
    </dependency>

[编辑]在阅读了spring的json路径匹配器的文档后,找到了一个适用于最新版jayway.jasonpath的quickfix。

.andExpect(jsonPath("$.password").doesNotExist())

5 个答案:

答案 0 :(得分:107)

我对新版本有同样的问题。在我看来,doesNotExist()函数将验证该键不在结果中:

.andExpect(jsonPath("$.password").doesNotExist())

答案 1 :(得分:3)

@JsonIgnore的行为符合预期,没有在json输出中生成密码,那么你怎么能期望测试你明确从输出中排除的东西?

该行:

.andExpect(jsonPath("$.property", is("some value")));

甚至测试该属性为null:

.andExpect(jsonPath("$.property").value(IsNull.nullValue()));

对应于json:

{
...
"property": "some value",
...
}

其中重要的部分是左侧,即&#34;属性的存在&#34;:

相反,@ JsonIgnore根本没有在输出中产生属性,所以你不能期望它不在测试中或生产输出中。 如果你不想在输出中使用该属性,那很好,但你不能期望它在测试中。 如果你想在输出中使用它(在prod和test中都是空的)你想在中间创建一个静态的Mapper方法,它不会将属性的值传递给json对象:

Mapper.mapPersonToRest(User user) {//exclude the password}

然后你的方法是:

@RequestMapping(value="/users/{userId}", method= RequestMethod.GET)
public ResponseEntity<UserResource> getUser(@PathVariable Long userId) {
    logger.info("Request arrived for getUser() with params {}", userId);
    User user = Mapper.mapPersonToRest(userService.getUserById(userId));
    if(user != null) {
        UserResource userResource = new UserResourceAsm().toResource(user);
        return new ResponseEntity<>(userResource, HttpStatus.OK);
    } else {
        return new ResponseEntity<>(HttpStatus.NOT_FOUND);
    }
}

此时,如果您期望Mapper.mapPersonToRest返回具有空密码的用户,则可以对此方法编写正常的单元测试。

P.S。当然密码是在DB上加密的,对吗? ;)

答案 2 :(得分:0)

doesNotHaveJsonPath用于检查它是否不在json正文中

答案 3 :(得分:0)

我想重用我用来测试所提供参数的相同代码,因为它缺少参数,这就是我想到的

  @Test
  void testEditionFoundInRequest() throws JsonProcessingException {
    testEditionWithValue("myEdition");
  }

  @Test
  void testEditionNotFoundInRequest() {
    try {
      testEditionWithValue(null);
      throw new RuntimeException("Shouldn't pass");
    } catch (AssertionError | JsonProcessingException e) {
      var msg = e.getMessage();
      assertTrue(msg.contains("No value at JSON path"));
    }
  }


  void testEditionWithValue(String edition) {   
   var HOST ="fakeHost";
   var restTemplate = new RestTemplate();
   var myRestClientUsingRestTemplate = new MyRestClientUsingRestTemplate(HOST, restTemplate);
   MockRestServiceServer mockServer;
   ObjectMapper objectMapper = new ObjectMapper();
   String id = "userId";
   var mockResponse = "{}";

   var request = new MyRequest.Builder(id).edition(null).build();
   mockServer = MockRestServiceServer.bindTo(restTemplate).bufferContent().build();

   mockServer
        .expect(method(POST))

        // THIS IS THE LINE I'd like to say "NOT" found
        .andExpect(jsonPath("$.edition").value(edition))
        .andRespond(withSuccess(mockResponse, APPLICATION_JSON));

    var response = myRestClientUsingRestTemplate.makeRestCall(request);
  } catch (AssertionError | JsonProcessingException e) {
    var msg = e.getMessage();
    assertTrue(msg.contains("No value at JSON path"));
  }

答案 4 :(得分:0)

存在但具有<?xml version="1.0" encoding="utf-8"?> <androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:fillViewport="true" android:layout_height="match_parent"> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> <androidx.cardview.widget.CardView android:id="@+id/cardView" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginStart="24dp" android:layout_marginTop="40dp" android:layout_marginEnd="24dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/tv_rate" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="40dp" android:layout_marginTop="16dp" android:text="@string/fragment_ship_detail_rate" android:textColor="@color/text_links" android:textSize="24sp" android:textStyle="bold" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/tv_review" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="40dp" android:layout_marginTop="8dp" android:textColor="@color/text_links" android:textSize="12sp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/tv_rate" tools:text="@string/ship_detail_review_count" /> <TextView android:id="@+id/tv_rateTotal" android:layout_width="48dp" android:layout_height="48dp" android:layout_marginTop="16dp" android:layout_marginEnd="32dp" android:background="@color/blue" android:gravity="center" android:textColor="@color/text_ship_rate" android:textSize="18sp" android:textStyle="bold" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" tools:text="6.7"> </TextView> <androidx.recyclerview.widget.RecyclerView android:id="@+id/rv_rate" android:layout_width="match_parent" android:layout_height="wrap_content" android:clipToPadding="false" android:paddingTop="8dp" android:paddingBottom="36dp" android:scrollbars="vertical" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/tv_review" tools:itemCount="5" tools:listitem="@layout/row_ship_score" /> </androidx.constraintlayout.widget.ConstraintLayout> </androidx.cardview.widget.CardView> <TextView android:id="@+id/tv_comment" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="24dp" android:layout_marginTop="40dp" android:text="@string/ship_detail_review_comments" android:textColor="@color/text_links" android:textSize="24sp" android:textStyle="bold" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/cardView" /> <androidx.recyclerview.widget.RecyclerView android:id="@+id/rv_comment" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="16dp" android:clipToPadding="false" android:paddingTop="8dp" android:paddingBottom="36dp" android:scrollbars="vertical" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/tv_comment" tools:itemCount="3" tools:listitem="@layout/row_ship_comment" /> <androidx.constraintlayout.widget.Group android:visibility="gone" android:id="@+id/gp_rate_comment" android:layout_width="wrap_content" android:layout_height="wrap_content" app:constraint_referenced_ids="cardView,tv_comment,rv_comment" /> <FrameLayout android:id="@+id/view_empty_State" android:layout_width="0dp" android:layout_height="0dp" android:background="@color/white_solid" android:orientation="horizontal" android:visibility="visible" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:layout_marginStart="32dp" android:layout_marginEnd="32dp" android:orientation="vertical"> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" app:srcCompat="@drawable/ic_no_rate_comment" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:layout_marginTop="16dp" android:gravity="center" android:text="@string/fragment_ship_review_no_review_comment" /> </LinearLayout> </FrameLayout> <include android:id="@+id/loading_view" layout="@layout/loading_view" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/white_solid" android:orientation="horizontal" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout> </androidx.core.widget.NestedScrollView> 值的属性与根本不存在的属性之间是有区别的。

如果在非null值不存在的情况下测试仅失败 ,请使用:

null

如果该属性存在后测试应立即失败,即使使用.andExpect(jsonPath("password").doesNotExist()) 值,也请使用:

null