[Testing] TDD/ API ๋ฌธ์ํ
Categories: Spring
๐ ๊ฐ์ธ์ ์ธ ๊ณต๊ฐ์ผ๋ก ๊ณต๋ถ๋ฅผ ๊ธฐ๋กํ๊ณ ๋ณต์ตํ๊ธฐ ์ํด ์ฌ์ฉํ๋ ๋ธ๋ก๊ทธ์
๋๋ค.
์ ํํ์ง ์์ ์ ๋ณด๊ฐ ์์ ์ ์์ผ๋ ์ฐธ๊ณ ๋ฐ๋๋๋ค :๐ธ
[ํ๋ฆฐ ๋ด์ฉ์ ๋๊ธ๋ก ๋จ๊ฒจ์ฃผ์๋ฉด ๋ณต๋ฐ์ผ์ค๊ฑฐ์์]
TDD(Test Driven Development, ํ ์คํธ ์ฃผ๋๊ฐ๋ฐ)
TDD์ ๊ฐ๋ ์ ํ๋ง๋๋ก ์์ฝํ์๋ฉด โํ ์คํธ๋ฅผ ๋จผ์ ํ๊ณ ๊ตฌํ์ ๊ทธ๋ค์์ ํ๋คโ์ด๋ค.
TDD๋ ๋ชจ๋ ์์ ์ด์ ์ ํ ์คํธ๋ฅผ ๋จผ์ ํ๋ค.
- ์ดํด ๋น์ฌ์๋ค ๊ฐ์ ์์ง๋ ์๊ตฌ ์ฌํญ๊ณผ ์ค๊ณ๋ ํ๋ฉด(UI ์ค๊ณ์ ๋ฑ) ๋ฑ์ ๊ธฐ๋ฐ์ผ๋ก ๋๋ฉ์ธ ๋ชจ๋ธ์ ๋์ถํฉ๋๋ค.
- ๋์ถ๋ ๋๋ฉ์ธ ๋ชจ๋ธ์ ํตํด ํด๋ผ์ด์ธํธ์ ์์ฒญ์ ๋ฐ์๋ค์ด๋ ์๋ํฌ์ธํธ์ ๋น์ฆ๋์ค ๋ก์ง, ๋ฐ์ดํฐ ์ก์ธ์ค๋ฅผ ์ํ ํด๋์ค์ ์ธํฐํ์ด์ค ๋ฑ์ ์ค๊ณํด์ ํฐ ๊ทธ๋ฆผ์ ๊ทธ๋ ค๋ด ๋๋ค.
- ํด๋์ค ์ค๊ณ๋ฅผ ํตํด ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ํ ํฐ ๊ทธ๋ฆผ์ ๊ทธ๋ ค๋ณด์๋ค๋ฉด ํด๋์ค์ ์ธํฐํ์ด์ค์ ํฐ ํ์ ์์ฑํฉ๋๋ค.
- ํด๋์ค์ ์ธํฐํ์ด์ค์ ํฐ ํ์ด ์์ฑ๋์๋ค๋ฉด ํด๋์ค์ ์ธํฐํ์ด์ค ๋ด์ ๋ฉ์๋๋ฅผ ์ ์ํ๋ฉด์ ์ธ๋ถ ๋์์ ๊ณ ๋ฏผํ๊ณ , ์ฝ๋๋ก ๊ตฌํํฉ๋๋ค.
- ํด๋น ๋ฉ์๋์ ๊ธฐ๋ฅ ๊ตฌํ์ด ๋๋ฌ๋ค๋ฉด ๊ตฌํํ ๊ธฐ๋ฅ์ด ์ ๋์ํ๋์ง ํ ์คํธํฉ๋๋ค.
- ํ ์คํธ์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค๋ฉด ๊ตฌํํ ์ฝ๋๋ฅผ ๋๋ฒ๊น ํ๋ฉด์ ๋ฌธ์ ์ ์์ธ์ ์ฐพ์ต๋๋ค.
โ๏ธ TDD ๊ด์ ์์ ๋๋๋ฌ์ง๋ ์ ํ ๊ฐ์ง๋ 3๋ฒ ~ 6๋ฒ์ ๊ณผ์ ์์ ๊ตฌํ์ด ๋จผ์ ๊ณ , ํ ์คํธ๊ฐ ๋์ค์ด๋ผ๋ ์
โ๏ธ TDD๋ฅผ ์ํ์ ๋๋ 6๋ฒ์ด ์ค์ด๋ค๊ธด ํ๋๋ฐ ํด์ธ์์๋ ๊ทธ๋ ๊ณ ๋ณต์ก๋๋ง ์ฌ๋ผ๊ฐ์ง ํฌ๊ฒ ์ฅ์ ์ด ์์ด์ ์ฌ์ฉ์ ํ์ง ์๋ ์ถ์ธ๋ผ๊ณ ํจ.
โญ TDD์ ๊ฐ๋ฐ ๋ฐฉ์์ โ์คํจํ๋ ํ
์คํธ โ ์คํจํ๋ ํ
์คํธ๋ฅผ ์ฑ๊ณตํ ๋งํผ์ ๊ธฐ๋ฅ ๊ตฌํ โ ์ฑ๊ณตํ๋ ํ
์คํธ โ ๋ฆฌํฉํ ๋ง โ ์คํจํ๋ ํ
์คํธ์ ์ฑ๊ณตํ๋ ํ
์คํธ ํ์ธ
โ์ด๋ผ๋ ํ๋ฆ์ ๋ฐ๋ณตํ๋ค.
TDD์ ํน์ง ์ ๋ฆฌ
- TDD๋ ๋ชจ๋ ์กฐ๊ฑด์ ๋ง์กฑํ๋ ํ ์คํธ๋ฅผ ๋จผ์ ์งํํ ๋ค์ ์กฐ๊ฑด์ ๋ง์กฑํ์ง ์๋ ํ ์คํธ๋ฅผ ๋จ๊ณ์ ์ผ๋ก ์งํํ๋ฉด์ ์คํจํ๋ ํ ์คํธ๋ฅผ ์ ์ง์ ์ผ๋ก ์ฑ๊ณต์ํจ๋ค.
- TDD๋ ํ
์คํธ ์คํ ๊ฒฐ๊ณผ๊ฐ โ
failed
โ์ธ ํ ์คํธ ์ผ์ด์ค๋ฅผ ์ง์์ ์ผ๋ก ๊ทธ๋ฆฌ๊ณ ๋จ๊ณ์ ์ผ๋ก ์์ ํ๋ฉด์ ํ ์คํธ ์ผ์ด์ค ์คํ ๊ฒฐ๊ณผ๊ฐ โpassed
โ๊ฐ ๋๋๋ก ๋ง๋ค๊ณ ์์ต๋๋ค. - TDD๋ ํ
์คํธ๊ฐ โ
passed
โ ๋ ๋งํผ์ ์ฝ๋๋ง ์ฐ์ ์์ฑํ๋ค. - TDD๋ โ
์คํจํ๋ ํ ์คํธ โ ์คํจํ๋ ํ ์คํธ๋ฅผ ์ฑ๊ณตํ ๋งํผ์ ๊ธฐ๋ฅ ๊ตฌํ โ ์ฑ๊ณตํ๋ ํ ์คํธ โ ๋ฆฌํฉํ ๋ง โ ์คํจํ๋ ํ ์คํธ์ ์ฑ๊ณตํ๋ ํ ์คํธ ํ์ธ
โ์ด๋ผ๋ ํ๋ฆ์ ๋ฐ๋ณต
์์์ TDD ๋ฐฉ์์ผ๋ก ์งํํ ํจ์ค์๋ ์ ํจ์ฑ ๊ฒ์ฆ ๊ธฐ๋ฅ์ ๊ตฌํ์ ๋ํ ์ค๋ช ์์ฒด๋ ๊ธธ์ง๋ง ์ค์ TDD ๋ฐฉ์์ผ๋ก ์ ์งํ๋๋ค๋ฉด ํ ์คํธ์ ๊ธฐ๋ฅ ๊ตฌํ, ๋ฆฌํฉํ ๋ง๊น์ง ๋น ๋ฅด๊ฒ ์งํ์ด ๊ฐ๋ฅ.
API ๋ฌธ์ํ
- API ๋ฌธ์ํ๋ ํด๋ผ์ด์ธํธ๊ฐ REST API ๋ฐฑ์๋ ์ ํ๋ฆฌ์ผ์ด์ ์ ์์ฒญ์ ์ ์กํ๊ธฐ ์ํด์ ์์์ผ ๋๋ ์์ฒญ ์ ๋ณด(์์ฒญ URL(๋๋ URI), request body, query parameter ๋ฑ)๋ฅผ ๋ฌธ์๋ก ์ ์ ๋ฆฌํ๋ ๊ฒ์ ์๋ฏธ
- REST API ๊ธฐ๋ฐ์ ๋ฐฑ์๋ ์ ํ๋ฆฌ์ผ์ด์ ์ ํด๋ผ์ด์ธํธ ์ชฝ์์ ์ฌ์ฉํ๋ ค๋ฉด API ์ฌ์ฉ์ ์ํ ์ด๋ค ์ ๋ณด๊ฐ ํ์ํจ โ API ์ฌ์ฉ์ ์ํ ์ด๋ค ์ ๋ณด๊ฐ ๋ด๊ฒจ ์๋ ๋ฌธ์๋ฅผ API ๋ฌธ์ ๋๋ API ์คํ(์ฌ์, Specification)
- ๊ฐ๋ฐ์ด ๋ค์ด๊ฐ๊ธฐ์ ์ ๋ฌธ์๊ฐ ๋์์ผ ํจ โ ๊ธฐํ๋จ๊ณ์์ ์ค๊ณ๋ฅผ ํจ.
Spring Rest Docs vs Swagger
Swagger
- ํน์ง
- ์ ๋ํ ์ด์ ๊ธฐ๋ฐ์ API ๋ฌธ์ํ ๋ฐฉ
- ์ ํ๋ฆฌ์ผ์ด์ ์ฝ๋์ ๋ฌธ์ํ๋ฅผ ์ํ ์ ๋ํ ์ด์ ๋ค์ด ํฌํจ๋๋ค.
- API ๋ฌธ์์ API ์ฝ๋ ๊ฐ์ ์ ๋ณด ๋ถ์ผ์น ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์๋ค.
- API ํด๋ก์จ์ ๊ธฐ๋ฅ์ ํ์ฉํ ์ ์๋ค.
- ๋ฌธ์ํ ๋ฟ๋ง ์๋๋ผ ํ ์คํธ ํด์ ๊ธฐ๋ฅ ์๊ณ UI๊ฐ ๊น๋ํด์ ๋ฌธ์๋ฅผ ํตํด ํ์ธํ๊ธฐ ์ข์.
- BUT, ๊ธฐ๋ฅ๋ณ ๋ถ๋ฆฌ๊ฐ ์๋๊ณ ๊ฐ๋ ์ฑ ๋ฐ ์ ์ง ๋ณด์์ฑ์ด ๋จ์ด์ง๋ค.
- ๋ฐฐํฌ์์ ์ ์ฝ๋๊ฐ ๋ค์ด๊ฐ๊ฒ ๋๋ค. โ ์ฃผ์๊ณผ ๋ก๊ทธ๋ฅผ ํฌํจ์ํค์ง ์๋ ๊ฒ์ด ์ผ๋ฐ์ ์ธ๋ฐ ๋ฐฐํฌํ ๋๋ ๋ฌธ์๊ฐ ํ์์๋๋ฐ ์ผ์ผ์ด ๋ฒ๊ฒจ๋ด์ผํ๋ค.
- ์์
-
Controller โ ๋ฌด์ํ ๋ง์ ์ ๋ํ ์ด์
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
@ApiOperation(value = "ํ์ ์ ๋ณด API", tags = {"Member Controller"}) // (1) @RestController @RequestMapping("/v11/swagger/members") @Validated @Slf4j public class MemberControllerSwaggerExample { private final MemberService memberService; private final MemberMapper mapper; public MemberControllerSwaggerExample(MemberService memberService, MemberMapper mapper) { this.memberService = memberService; this.mapper = mapper; } // (2) @ApiOperation(value = "ํ์ ์ ๋ณด ๋ฑ๋ก", notes = "ํ์ ์ ๋ณด๋ฅผ ๋ฑ๋กํฉ๋๋ค.") // (3) @ApiResponses(value = { @ApiResponse(code = 201, message = "ํ์ ๋ฑ๋ก ์๋ฃ"), @ApiResponse(code = 404, message = "Member not found") }) @PostMapping public ResponseEntity postMember(@Valid @RequestBody MemberDto.Post memberDto) { Member member = mapper.memberPostToMember(memberDto); member.setStamp(new Stamp()); // homework solution ์ถ๊ฐ Member createdMember = memberService.createMember(member); return new ResponseEntity<>( new SingleResponseDto<>(mapper.memberToMemberResponse(createdMember)), HttpStatus.CREATED); } ... ... // (4) @ApiOperation(value = "ํ์ ์ ๋ณด ์กฐํ", notes = "ํ์ ์๋ณ์(memberId)์ ํด๋นํ๋ ํ์์ ์กฐํํฉ๋๋ค.") @GetMapping("/{member-id}") public ResponseEntity getMember( @ApiParam(name = "member-id", value = "ํ์ ์๋ณ์", example = "1") // (5) @PathVariable("member-id") @Positive long memberId) { Member member = memberService.findMember(memberId); return new ResponseEntity<>( new SingleResponseDto<>(mapper.memberToMemberResponse(member)) , HttpStatus.OK); } ... ... }
-
DTO, Request Body๋ Response Body ๊ฐ์ DTO ํด๋์ค์๋ ์๋์ ์ฝ๋์ ๊ฐ์ด Swagger์ ์ ๋ํ ์ด์ ์ ์ผ์ผ์ด ์ถ๊ฐํด ์ฃผ์ด์ผ ํจ.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
@ApiModel("Member Post") // (1) @Getter public class MemberPostDto { // (2) @ApiModelProperty(notes = "ํ์ ์ด๋ฉ์ผ", example = "hgd@gmail.com", required = true) @NotBlank @Email private String email; // (3) @ApiModelProperty(notes = "ํ์ ์ด๋ฆ", example = "ํ๊ธธ๋", required = true) @NotBlank(message = "์ด๋ฆ์ ๊ณต๋ฐฑ์ด ์๋์ด์ผ ํฉ๋๋ค.") private String name; // (4) @ApiModelProperty(notes = "ํ์ ํด๋ํฐ ๋ฒํธ", example = "010-1111-1111", required = true) @Pattern(regexp = "^010-\\\\d{3,4}-\\\\d{4}$", message = "ํด๋ํฐ ๋ฒํธ๋ 010์ผ๋ก ์์ํ๋ 11์๋ฆฌ ์ซ์์ '-'๋ก ๊ตฌ์ฑ๋์ด์ผ ํฉ๋๋ค.") private String phone; }
-
-
Swagger API ๋ฌธ์ ํ๋ฉด ์
Postman์์ MemberControlle์ HTTP ์์ฒญ์ ์ ์กํ๋ฏ์ด [Execute] ๋ฒํผ์ ๋๋ฅด๋ฉด MemberController์ ์์ฒญ์ ์ ์ก๊ฐ๋ฅ
Spring Rest Docs
- ํน์ง
- ํ ์คํธ ์ฝ๋ ๊ธฐ๋ฐ์ API ๋ฌธ์ํ ๋ฐฉ์
- ์ ํ๋ฆฌ์ผ์ด์ ์ฝ๋์ ๋ฌธ์ํ๋ฅผ ์ํ ์ ๋ณด๋ค์ด ํฌํจ๋์ง ์๋๋ค.
- ํ ์คํธ ์ผ์ด์ค์ ์คํ์ด โpassedโ์ฌ์ผ API ๋ฌธ์๊ฐ ์์ฑ๋๋ค.
- ํ ์คํธ ์ผ์ด์ค๋ฅผ ๋ฐ๋์ ์์ฑํด์ผ ๋๋ค.
- API ํด๋ก์จ์ ๊ธฐ๋ฅ์ ์ ๊ณตํ์ง ์๋๋ค.
-
์์
- ์ฝ๋์ ๋ ธ๋ ๋ฐ์ค๋ถ๋ถ์ด ๋ฌธ์ํ ๊ด๋ จ์ฝ๋์ด๋ค.
- ํ ์คํธ ์ฝ๋ ๊ฒ์ฆ์ ๋ฌธ์ํ ์ฝ๋์ ํฌํจ๋์ด ์์.โ ๊ทธ๋ ๊ฒ ๋๋ฌธ์ ๋ฌธ์ํํ๋ ค๋ฉด ํ ์คํธ ์ฝ๋๊ฐ ํ์
-
Spring Rest Docs์ API ๋ฌธ์ํ๋ฉด ์
Spring Rest Docs
Spring Rest Docs์ API ๋ฌธ์ ์์ฑ ํ๋ฆ
- ํ
์คํธ ์ฝ๋ ์์ฑ
- ์ฌ๋ผ์ด์ค ํ ์คํธ ์ฝ๋ ์์ฑ โ ฐ. Spring Rest Docs๋ Controller์ ์ฌ๋ผ์ด์ค ํ ์คํธ์ ๋ฐ์ ํ ๊ด๋ จ์ด ์์
- API ์คํ ์ ๋ณด ์ฝ๋ ์์ฑ โ ฐ. ์ฌ๋ผ์ด์ค ํ ์คํธ ์ฝ๋ ๋ค์์ Controller์ ์ ์๋์ด ์๋ API ์คํ ์ ๋ณด(Request Body, Response Body, Query Parameter ๋ฑ)๋ฅผ ์ฝ๋๋ก ์์ฑ
- test ํ์คํฌ(task) ์คํ
- ์์ฑ๋ ์ฌ๋ผ์ด์ค ํ ์คํธ ์ฝ๋๋ฅผ ์คํํ๋ค. โ ฐ. ํ๋์ ํ ์คํธ ํด๋์ค๋ฅผ ์คํ์์ผ๋ ๋์ง๋ง ์ผ๋ฐ์ ์ผ๋ก Gradle์ ๋น๋ ํ์คํฌ(task)์ค ํ๋์ธ test task๋ฅผ ์คํ์์ผ์ API ๋ฌธ์ ์ค๋ํซ(snippet)์ ์ผ๊ด ์์ฑํ๋ค.
- ํ
์คํธ ์คํ ๊ฒฐ๊ณผ๊ฐ โ
passed
โ์ด๋ฉด ๋ค์ ์์ ์ ์งํํ๊ณ , โfailed
โ์ด๋ฉด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ํ ์คํธ ์ผ์ด์ค๋ฅผ ์์ ํ ํ, ๋ค์ ํ ์คํธ๋ฅผ ์งํํด์ผ ํ๋ค. โํ ์คํธ๋ฅผ ํต๊ณผํด์ผ ๋ฌธ์ ์์ ๊ฐ๋ฅ.
- API ๋ฌธ์ ์ค๋ํซ( .adoc ํ์ผ) ์์ฑ
- ํ
์คํธ ์ผ์ด์ค์ ํ
์คํธ ์คํ ๊ฒฐ๊ณผ๊ฐ โ
passed
โ์ด๋ฉด ํ ์คํธ ์ฝ๋์ ํฌํจ๋ API ์คํ ์ ๋ณด ์ฝ๋๋ฅผ ๊ธฐ๋ฐ์ผ๋ก API ๋ฌธ์ ์ค๋ํซ์ด.adoc
ํ์ฅ์๋ฅผ ๊ฐ์ง ํ์ผ๋ก ์์ฑ๋๋ค.
- ํ
์คํธ ์ผ์ด์ค์ ํ
์คํธ ์คํ ๊ฒฐ๊ณผ๊ฐ โ
์ค๋ํซ์ด๋? ์ค๋ํซ์ ํ ์คํธ ์ผ์ด์ค ํ๋๋น ํ๋์ ์ค๋ํซ์ด ์์ฑ๋๋ฉฐ, ์ฌ๋ฌ ๊ฐ์ ์ค๋ํซ์ ๋ชจ์์ ํ๋์ API ๋ฌธ์๋ฅผ ์์ฑ ๊ฐ๋ฅํ๋ค. API ๋ฌธ์ ์์ฑ
- ์์ฑ๋ API ๋ฌธ์ ์ค๋ํซ์ ๋ชจ์์ ํ๋์ API ๋ฌธ์๋ก ์์ฑํฉ๋๋ค.
- API ๋ฌธ์๋ฅผ HTML๋ก ๋ณํ
- ์์ฑ๋ API ๋ฌธ์๋ฅผ HTML ํ์ผ๋ก ๋ณํํฉ๋๋ค.
- HTML๋ก ๋ณํ๋ API ๋ฌธ์๋ HTML ํ์ผ ์์ฒด๋ฅผ ๊ณต์ ํ ์๋ ์๊ณ , URL์ ํตํด ํด๋น HTML์ ์ ์ํด์ ํ์ธํ ์ ์๋ค.
Spring Rest Docs ์ค์
-
build.gradle ์ค์
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
plugins { id 'org.springframework.boot' version '2.7.1' id 'io.spring.dependency-management' version '1.0.11.RELEASE' id "org.asciidoctor.jvm.convert" version "3.3.2" // (1) id 'java' } /* (1) .adoc ํ์ผ ํ์ฅ์๋ฅผ ๊ฐ์ง๋ AsciiDoc ๋ฌธ์๋ฅผ ์์ฑํด ์ฃผ๋ Asciidoctor๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํ ํ๋ฌ๊ทธ์ธ์ ์ถ๊ฐ */ group = 'com.springboot' version = '0.0.1-SNAPSHOT' sourceCompatibility = '11' repositories { mavenCentral() } ext { // (2) ext ๋ณ์์ set() ๋ฉ์๋๋ฅผ ์ด์ฉํด์ API ๋ฌธ์ ์ค๋ํซ์ด ์์ฑ๋ ๊ฒฝ๋ก๋ฅผ ์ง์ set('snippetsDir', file("build/generated-snippets")) } /* (3) AsciiDoctor์์ ์ฌ์ฉ๋๋ ์์กด ๊ทธ๋ฃน์ ์ง์ :asciidoctor task๊ฐ ์คํ๋๋ฉด ๋ด๋ถ์ ์ผ๋ก (3)์์ ์ง์ ํ โasciidoctorExtensionsโ๋ผ๋ ๊ทธ๋ฃน์ ์ง์ */ configurations { asciidoctorExtensions } dependencies { /* (4) org.springframework.restdocs:spring-restdocs-mockmvc'๋ฅผ ์ถ๊ฐํจ์ผ๋ก์จ spring-restdocs-core์ spring-restdocs-mockmvc ์์กด ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์ถ๊ฐ๋๋ค.*/ testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' /*(5) pring-restdocs-asciidoctor ์์กด ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ถ๊ฐ (3)์์ ์ง์ ํ asciidoctorExtensions ๊ทธ๋ฃน์ ์์กด ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ํฌํจ๋๋ค */ asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' implementation 'org.mapstruct:mapstruct:1.5.1.Final' annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.1.Final' implementation 'org.springframework.boot:spring-boot-starter-mail' implementation 'com.google.code.gson:gson' } // (6) :test task ์คํ ์, API ๋ฌธ์ ์์ฑ ์ค๋ํซ ๋๋ ํ ๋ฆฌ ๊ฒฝ๋ก๋ฅผ ์ค์ tasks.named('test') { outputs.dir snippetsDir useJUnitPlatform() } /* (7) sciidoctor task ์คํ ์, Asciidoctor ๊ธฐ๋ฅ์ ์ฌ์ฉํ๊ธฐ ์ํด :asciidoctor task์ asciidoctorExtensions์ ์ค์ */ tasks.named('asciidoctor') { configurations "asciidoctorExtensions" inputs.dir snippetsDir dependsOn test } /* (8) :build task ์คํ ์ ์ ์คํ๋๋ task :copyDocument task๊ฐ ์ํ๋๋ฉด index.html ํ์ผ์ด src/main/resources/static/docs ์ copy ๋๋ฉฐ, copy ๋ index.html ํ์ผ์ API ๋ฌธ์๋ฅผ ํ์ผ ํํ๋ก ์ธ๋ถ์ ์ ๊ณตํ๊ธฐ ์ํ ์ฉ๋๋ก ์ฌ์ฉ*/ task copyDocument(type: Copy) { dependsOn asciidoctor // (8-1) :asciidoctor task๊ฐ ์คํ๋ ํ์ task๊ฐ ์คํ๋๋๋ก ์์กด์ฑ์ ์ค์ from file("${asciidoctor.outputDir}") // (8-2) build/docs/asciidoc/" ๊ฒฝ๋ก์ ์์ฑ๋๋ index.html์ copy into file("src/main/resources/static/docs") // (8-3) src/main/resources/static/docs" ๊ฒฝ๋ก๋ก index.html์ ์ถ๊ฐ } build { dependsOn copyDocument // (9):build task๊ฐ ์คํ๋๊ธฐ ์ ์ :copyDocument task๊ฐ ๋จผ์ ์ํ๋๋๋ก ํจ. } // (10) ์ ํ๋ฆฌ์ผ์ด์ ์คํ ํ์ผ์ด ์์ฑํ๋ :bootJar task ์ค์ bootJar { dependsOn copyDocument // (10-1) :bootJar task ์คํ ์ ์ :copyDocument task๊ฐ ์คํ๋๋๋ก ์์กด์ฑ์ ์ค์ from ("${asciidoctor.outputDir}") { into 'static/docs' /*(10-2) Asciidoctor ์คํ์ผ๋ก ์์ฑ๋๋ index.html ํ์ผ์ jar ํ์ผ ์์ ์ถ๊ฐ jar ํ์ผ์ index.html์ ์ถ๊ฐํด ์ค์ผ๋ก์จ ์น ๋ธ๋ผ์ฐ์ ์์ ์ ์ (http://localhost:8080/docs/index.html) ํ, API ๋ฌธ์๋ฅผ ํ์ธ ํ ์ ์๋ค.*/ } }
-
API ๋ฌธ์ ์ค๋ํซ์ ์ฌ์ฉํ๊ธฐ ์ํ ํ ํ๋ฆฟ API ๋ฌธ์ ์์ฑ
- Gradle ๊ธฐ๋ฐ ํ๋ก์ ํธ์์๋ ์๋ ๊ฒฝ๋ก์ ํด๋นํ๋ ๋๋ ํ ๋ฆฌ๋ฅผ ์์ฑํด ์ฃผ์ด์ผ ํฉ๋๋ค.
src/docs/asciidoc/
- ๋ค์์ผ๋ก
src/docs/asciidoc/
๋๋ ํ ๋ฆฌ ๋ด์ ๋น์ด์๋ ํ ํ๋ฆฟ ๋ฌธ์(index.adoc
)๋ฅผ ์์ฑํด ์ฃผ๋ฉด ๋๋ค.
- Gradle ๊ธฐ๋ฐ ํ๋ก์ ํธ์์๋ ์๋ ๊ฒฝ๋ก์ ํด๋นํ๋ ๋๋ ํ ๋ฆฌ๋ฅผ ์์ฑํด ์ฃผ์ด์ผ ํฉ๋๋ค.
์ฌ๋ผ์ด์คํ ์คํธ
- MemberController๊ฐ ์์ฒญ์ ์ ์ ๋ฌ๋ฐ๊ณ , ์๋ต์ ์ ์ ์กํ๋ฉฐ ์์ฒญ๊ณผ ์๋ต์ด ์ ์์ ์ผ๋ก ์ํ๋๋ฉด API ๋ฌธ์ ์คํ ์ ๋ณด๋ฅผ ์ ์ฝ์ด ๋ค์ฌ์ ์ ์ ํ ๋ฌธ์๋ฅผ ์ ์์ฑํ๋๋ ํ๋ ๊ฒ์ Testํ๋ฉด ๋จ
- MemberService์ MemberMapper์ ๋ฉ์๋๋ฅผ ํธ์ถํ์ง ์๋๋ก ๊ด๊ณ๋ฅผ ๋จ์ โ MemberService์ MemberMapper์ Mock Bean์ ์ฃผ์
1๏ธโฃ ์ ๋ํ ์ด์
@WebMvcTest
์ ๋ํ ์ด์ ์ Controller๋ฅผ ํ ์คํธํ๊ธฐ ์ํ ์ ์ฉ ์ ๋ํ ์ด์ , ๊ดํธ ์์๋ ํ ์คํธ ๋์ Controller ํด๋์ค๋ฅผ ์ง์ - @MockBean(JpaMetamodelMappingContext.class)
@SpringBootTest vs @WebMvcTest
๊ธฐ์กด์๋
@SpringBootTest
+@AutoConfigureMockMvc
์ ๋ํ ์ด์ ์ผ๋ก Controller์ ํ ์คํธ๋ฅผ ์งํ
@SpringBootTest
์@WebMvcTest
์ ์ฐจ์ด์ ์? ๋จผ์ @SpringBootTest
์ ๋ํ ์ด์ ์@AutoConfigureMockMvc
๊ณผ ํจ๊ป ์ฌ์ฉ๋์ด Controller๋ฅผ ํ ์คํธํ ์ ์๋๋ฐ, ํ๋ก์ ํธ์์ ์ฌ์ฉํ๋ ์ ์ฒด Bean์ ApplicationContext์ ๋ฑ๋กํ์ฌ ์ฌ์ฉํ๋ค. ํ๋ง๋๋ก ํ ์คํธ ํ๊ฒฝ์ ๊ตฌ์ฑํ๋ ๊ฒ์ ํธ๋ฆฌํ๊ธด ํ๋ฐ ์คํ ์๋๊ฐ ์๋์ ์ผ๋ก ๋๋ฆผ.
@WebMvcTest
์ ๋ํ ์ด์ ์ ๊ฒฝ์ฐ Controller ํ ์คํธ์ ํ์ํ Bean๋ง ApplicationContext์ ๋ฑ๋กํ๊ธฐ ๋๋ฌธ์ ์คํ ์๋๋ ์๋์ ์ผ๋ก ๋น ๋ฅด๋ค.๋ค๋ง, Controller์์ ์์กดํ๊ณ ์๋ ๊ฐ์ฒด๊ฐ ์๋ค๋ฉด ํด๋น ๊ฐ์ฒด์ ๋ํด์ Mock ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํ์ฌ ์์กด์ฑ์ ์ผ์ผ์ด ์ ๊ฑฐํด ์ฃผ์ด์ผ ํฉ๋๋ค.
๊ฒฐ๊ณผ์ ์ผ๋ก @SpringBootTest๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค๊น์ง ์์ฒญ ํ๋ก์ธ์ค๊ฐ ์ด์ด์ง๋ ํตํฉ ํ ์คํธ์ ์ฃผ๋ก ์ฌ์ฉ๋๊ณ , @WebMvcTest๋ Controller๋ฅผ ์ํ ์ฌ๋ผ์ด์ค ํ ์คํธ์ ์ฃผ๋ก ์ฌ์ฉํ๋ค.
2๏ธโฃ MemberController์ postMember() ํธ๋ค๋ฌ ๋ฉ์๋์ ๋ํ API ์คํ์ ๋ณด ์ถ๊ฐ
- ๋
ธ๋๋ฐ์ค ๋ด์ document(โฆ) ๋ฉ์๋๋ API ์คํ ์ ๋ณด๋ฅผ ์ ๋ฌ๋ฐ์์ ์ค์ง์ ์ธ ๋ฌธ์ํ ์์
์ ์ํํ๋
RestDocumentationResultHandler
ํด๋์ค์์ ๊ฐ์ฅ ํต์ฌ ๊ธฐ๋ฅ์ ํ๋ ๋ฉ์๋ document()
๋ฉ์๋์ ์ฒซ ๋ฒ์งธ ํ๋ผ๋ฏธํฐ๋ API ๋ฌธ์ ์ค๋ํซ์ ์๋ณ์ ์ญํ ์ ํ๋ฉฐ, โpost-member
โ๋ก ์ง์ ํ๊ธฐ ๋๋ฌธ์ ๋ฌธ์ ์ค๋ํซ์post-member
๋๋ ํ ๋ฆฌ ํ์์ ์์ฑ๋๋ค.-
getRequestPreProcessor() ์ getResponsePreProcessor()
๋ฌธ์ ์ค๋ํซ์ ์์ฑํ๊ธฐ ์ ์ request์ response์ ํด๋นํ๋ ๋ฌธ์ ์์ญ์ ์ ์ฒ๋ฆฌํ๋ ์ญํ ์ ํ๋๋ฐ ์๋์ ๊ฐ์ด ๊ณตํตํํ ํ, ๋ชจ๋ ํ ์คํธ ์ผ์ด์ค์์ ์ฌ์ฌ์ฉํ ์ ์๊ฒ ํจ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
package com.springboot.util; import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; public interface ApiDocumentUtils { static OperationRequestPreprocessor getRequestPreProcessor() { return preprocessRequest(prettyPrint()); } static OperationResponsePreprocessor getResponsePreProcessor() { return preprocessResponse(prettyPrint()); } }
requestFields(โฆ)
๋ ๋ฌธ์๋ก ํํ๋ request body๋ฅผ ์๋ฏธ,List<FieldDescriptor>
์ ์์์ธFieldDescriptor
๊ฐ์ฒด๊ฐ request body์ ํฌํจ๋ ๋ฐ์ดํฐ๋ฅผ ํํresponseHeaders(โฆ)
๋ ๋ฌธ์๋ก ํํ๋ response header๋ฅผ ์๋ฏธ,ํ๋ผ๋ฏธํฐ๋ก ์ ๋ฌ๋๋HeaderDescriptor
๊ฐ์ฒด๊ฐ response header๋ฅผ ํํ- ์คํ ๊ฒฐ๊ณผ๊ฐ โ
passed
โ์ด๋ฉด ์ฐ๋ฆฌ๊ฐ ์์ฑํ API ์คํ ์ ๋ณด๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ๋ฌธ์ ์ค๋ํซ์ด ๋ง๋ค์ด์ง.
3๏ธโฃ MemberController์ patchMember() ํธ๋ค๋ฌ ๋ฉ์๋์ ๋ํ API ์คํ์ ๋ณด ์ถ๊ฐ
memberId
์ ๊ฒฝ์ฐ,path variable
์ ๋ณด๋ก memberId๋ฅผ ์ ๋ฌ๋ฐ๊ธฐ ๋๋ฌธ์ MemberDto.Patch DTO ํด๋์ค์์ request body์ ๋งคํ๋์ง ์๋ ์ ๋ณด โ ๊ทธ๋์ (2)์ ๊ฐ์ด ignored()๋ฅผ ์ถ๊ฐํด์ API ์คํ ์ ๋ณด์์ ์ ์ธ- ํ์์ ๋ณด๋ ์ ํ์ ์ผ๋ก ์์ ํด์ผ ํ๋ฏ๋ก optional()์ ์ถ๊ฐํ์ฌ ์ ํ์ ๋ณด๋ก ์ค์ ํจ.
Leave a comment