ã·ãªãŒãºãSpring Bootã§ã®ã¢ããªã±ãŒã·ã§ã³ã®ãã¹ãé§åéçºãã®2çªç®ã®èšäºãšä»åã¯ãçµ±åãã¹ãã®éèŠãªåŽé¢ã§ããããŒã¿ããŒã¹ãžã®ã¢ã¯ã»ã¹ã®ãã¹ãã«ã€ããŠèª¬æããŸãã ãã¹ãã«ããããŒã¿ã¢ã¯ã»ã¹ã®å°æ¥ã®ãµãŒãã¹ã®ã€ã³ã¿ãŒãã§ã€ã¹ã決å®ããæ¹æ³ããã¹ãã«çµã¿èŸŒã¿ã®ã€ã³ã¡ã¢ãªããŒã¿ããŒã¹ã䜿çšããæ¹æ³ããã©ã³ã¶ã¯ã·ã§ã³ãæäœãããããŒã¿ããŒã¹ã«ãã¹ãããŒã¿ãã¢ããããŒãããæ¹æ³ã«ã€ããŠèª¬æããŸãã
äžè¬ã«TDDãšãã¹ãã«ã€ããŠã¯ããŸã話ããŸãããæåã®èšäº- ãã©ã³ã¯ã«ãã©ããããæ§ç¯ããæ¹æ³ããŸãã¯Spring Boot / geekãã¬ãžã³ã®ã¢ããªã±ãŒã·ã§ã³ã®ãã¹ãé§åéçºã«ã€ããŠèªãã§ãã ããã
ååãšåæ§ã«ãå°ããªçè«çãªéšåããå§ããŠããšã³ãããŒãšã³ãã®ãã¹ãã«é²ã¿ãŸãã
ãã©ãããã®ãã¹ã
ãŸãã ãã¹ããã©ãããããã¹ããã©ããããªã©ããã¹ãã«ãããéèŠãªãšã³ãã£ãã£ã®å°ãããªãããå¿
èŠãªèª¬æã
ïŒ å®å°è©Šéšãã©ãããããååŸïŒ
ãã¹ããã©ãããã¯ããã¹ããè€æ°ã®ã¬ãã«ã§ç·šæãããŠããå Žåã®ã¢ãããŒãã§ãã
- UI ïŒãŸãã¯ãšã³ãããŒãšã³ãã E2E ïŒãã¹ãã¯å°æ°ã§ãããäœéã§ãããå®éã®ã¢ããªã±ãŒã·ã§ã³ããã¹ãããŸã-ã¢ãã¯ã察å¿ãããã¹ãã¯ãããŸããã ããžãã¹ã¯ãã°ãã°ãã®ã¬ãã«ã§èãããã¹ãŠã®BDDãã¬ãŒã ã¯ãŒã¯ãããã«ååšããŸãïŒåã®èšäºã®Cucumberãåç
§ïŒã
- ãã®åŸã çµ±åãã¹ã ïŒãµãŒãã¹ãã³ã³ããŒãã³ã-ããããç¬èªã®çšèªããããŸãïŒãç¶ããã·ã¹ãã ã®ç¹å®ã®ã³ã³ããŒãã³ãïŒãµãŒãã¹ïŒã«ãã§ã«çŠç¹ãåœãŠãmoki / doublesãä»ããŠä»ã®ã³ã³ããŒãã³ãããåé¢ããŸãããå®éã®å€éšã·ã¹ãã ãšã®çµ±åã確èªããŸã-ãããã®ãã¹ãã¯æ¥ç¶ãããŠããŸãããŒã¿ããŒã¹ã«RESTãªã¯ãšã¹ããéä¿¡ããã¡ãã»ãŒãžãã¥ãŒãæäœããŸãã å®éããããã¯ããžãã¹ããžãã¯ãšå€çãšã®çµ±åãæ€èšŒãããã¹ãã§ãã
- äžçªäžã«ã¯ãæå°éã®ã³ãŒããããã¯ïŒã¯ã©ã¹ãã¡ãœããïŒãå®å
šã«åé¢ããŠãã¹ãããã¯ã€ãã¯ãŠããããã¹ãããããŸãã
ãŠããããã¹ãã®äžçã§ã¯ãã¬ãŒã ã¯ãŒã¯ã«é¢ããç¥èããŸã£ããååšããªããããSpringã¯åã¬ãã«ã®ãã¹ãã®äœæãæ¯æŽããŸãã E2Eãã¹ããäœæããåŸãSpringãã³ã³ãããŒã©ãŒã®ãããªçŽç²ã«ãçµ±åãããããã®ã§ãããåç¬ã§ãã¹ãã§ããããšã瀺ããŸãã
ãããããã©ãããã®æäžéšããå§ããŸããã-æ¬æ Œçãªã¢ããªã±ãŒã·ã§ã³ãéå§ããŠãã¹ãããé
ãUIãã¹ãã§ãã
ãšã³ãããŒãšã³ãã®ãã¹ã
ãã®ãããæ°ããæ©èœïŒ
Feature: A list of available cakes Background: catalogue is updated Given the following items are promoted | Title | Price | | Red Velvet | 3.95 | | Victoria Sponge | 5.50 | Scenario: a user visiting the web-site sees the list of items Given a new user, Alice When she visits Cake Factory web-site Then she sees that "Red Velvet" is available with price £3.95 And she sees that "Victoria Sponge" is available with price £5.50
ãããŠãããã«ããã«èå³æ·±ãåŽé¢ããããŸã-ã¡ã€ã³ããŒãžã®ãããã€ã«ã€ããŠãåã®ãã¹ãã§äœããã¹ããïŒ ã¡ã€ã³ããŒãžã§ãµã€ããç«ã¡äžããåŸã¯ãæšæ¶ã§ã¯ãªããã£ã¬ã¯ããªãæ¢ã«ååšããããã«ãªããŸãã åäžã®çãã¯ãããŸãããç§ã¯èšãã§ããã-ããã¯ç¶æ³ã«äŸåããŸãã ããããäž»ãªã¢ããã€ã¹-ãã¹ãã«æ·»ä»ããªãã§ãã ããïŒ é¢é£æ§ã倱ããããåé€ããèªã¿ãããããããã«æžãçŽããŸãã ç¹ã«E2Eãã¹ã-ããã¯ãå®éã 掻çºã§ææ°ã®ä»æ§ã§ããã¹ãã§ãã ç§ã®å Žåã以åã®ã¹ããããããã€ã䜿çšããååšããªããã¹ããè¿œå ããŠãå€ããã¹ããåé€ããæ°ãããã¹ãã«çœ®ãæããŸããã
ä»ãç§ã¯éèŠãªãã€ã³ãã«æ¥ãŸãã-ããŒã¿ãä¿åããããã®æè¡ã®éžæã ãªãŒã³ã¢ãããŒãã«åŸã£ãŠãç§ã¯éžæãæåŸã®ç¬éãŸã§å»¶æããããšæããŸãããªã¬ãŒã·ã§ãã«ã¢ãã«ãã©ãããäžè²«æ§ã®èŠä»¶ããã©ã³ã¶ã¯ã·ã§ã³æ§ã確å®ã«ããããšãã§ãã äžè¬ã«ãããã«ã¯è§£æ±ºçããããŸããããšãã°ã ãã¹ããã€ã³ãããŸããŸãªã¡ã¢ãªå
ã¹ãã¬ãŒãžã®äœæã§ããããããŸã§ã¯ãèšäºãè€éã«ããããã«ãã¯ãããžãéžæãããã¯ãããŸãã-ãªã¬ãŒã·ã§ãã«ããŒã¿ããŒã¹ã ãã ããããŒã¿ããŒã¹ãéžæããå¯èœæ§ãå°ãªããšãä¿æããããã«ãæœè±¡å-Spring Data JPAãè¿œå ããŸãã JPAèªäœã¯ãªã¬ãŒã·ã§ãã«ããŒã¿ããŒã¹ã«ã¢ã¯ã»ã¹ããããã®éåžžã«æœè±¡çãªä»æ§ã§ãããSpring Dataã¯ãããããã«ç°¡åã«äœ¿çšã§ããããã«ããŸãã
Spring Data JPAã¯ããã©ã«ãã§ãããã€ããŒãšããŠHibernateã䜿çšããŸãããEclipseLinkãMyBatisãªã©ã®ä»ã®ãã¯ãããžãŒããµããŒãããŸãã Java Persistence APIã«ããŸãæ
£ããŠããªã人ã«ãšã£ãŠã¯ãJPAã¯ã€ã³ã¿ãŒãã§ãŒã¹ã®ãããªãã®ã§ãããHibernateã¯ãããå®è£
ããã¯ã©ã¹ã§ãã
ããã§ãJPAã®ãµããŒããè¿œå ããããã«ãããã€ãã®äŸåé¢ä¿ãè¿œå ããŸããã
implementation('org.springframework.boot:spring-boot-starter-data-jpa') runtime('com.h2database:h2')
ããŒã¿ããŒã¹ãšããŠã H2 -Javaã§äœæãããçµã¿èŸŒã¿ããŒã¿ããŒã¹ã䜿çšããŸããããã¯ãã€ã³ã¡ã¢ãªã¢ãŒãã§åäœããæ©èœãåããŠããŸãã
Spring Data JPAã䜿çšããŠãããŒã¿ã«ã¢ã¯ã»ã¹ããããã®ã€ã³ã¿ãŒãã§ãŒã¹ãããã«å®çŸ©ããŸãã
interface CakeRepository extends CrudRepository<CakeEntity, String> { }
ãããŠãã®æ¬è³ªïŒ
@Entity @Builder @AllArgsConstructor @Table(name = "cakes") class CakeEntity { public CakeEntity() { } @Id @GeneratedValue(strategy = GenerationType.IDENTITY) Long id; @NotBlank String title; @Positive BigDecimal price; @NotBlank @NaturalId String sku; boolean promoted; @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; CakeEntity cakeEntity = (CakeEntity) o; return Objects.equals(title, cakeEntity.title); } @Override public int hashCode() { return Objects.hash(title); } }
ãšã³ãã£ãã£ã®èª¬æã«ã¯ãããã»ã©æçœã§ã¯ãªããã®ãããã€ããããŸãã
sku
ãã£ãŒã«ãã®@NaturalId
ã ãã®ãã£ãŒã«ãã¯ããšã³ãã£ãã£ã®ç䟡æ§ããã§ãã¯ããããã®ãèªç¶ãªèå¥åããšããŠäœ¿çšãããŸã@Id
/ hashCode
ã¡ãœããã§ãã¹ãŠã®ãã£ãŒã«ããŸãã¯@Id
ãã£ãŒã«ãã䜿çšããããšã¯ããããã¢ã³ããã¿ãŒã³ã§ãã ãšã³ãã£ãã£ã®ç䟡æ§ãæ£ããæ€èšŒããæ¹æ³ã«ã€ããŠã¯ãããšãã°ããã§èª¬æãããŠããŸã ã- å®åã³ãŒãã®æ°ãå°ãªããšããããã«æžããããã«ã Project Lombok -Javaçšã®æ³šéããã»ããµã䜿çšããŸãã
@Builder
ã¯ã©ã¹ã®ãã«ããŒãèªåçã«çæãã@Builder
ãªã©ãããŸããŸãªäŸ¿å©ãªãã®ãè¿œå ããŠããã¹ãŠã®ãã£ãŒã«ãã®ã³ã³ã¹ãã©ã¯ã¿ãŒãäœæã§ããŸãã
ã€ã³ã¿ãŒãã§ã€ã¹ã®å®è£
ã¯ãSpring Dataã«ãã£ãŠèªåçã«æäŸãããŸãã
ãã©ããããäžããŠ
ä»ããããã©ãããã®æ¬¡ã®ã¬ãã«ã«è¡ããšãã§ãã çµéšåãšããŠããæçµç®æšããšæ°æ©èœã®å¢çã決å®ã§ããããã«ãããããåžžã«e2eãã¹ãããå§ããããšããå§ãããŸãã ããã以äžå³å¯ãªã«ãŒã«ã¯ãããŸããã ãŠãããã¬ãã«ã«ç§»è¡ããåã«ããŸãçµ±åãã¹ããäœæããå¿
èŠã¯ãããŸããã ãããŠãã®å Žåããã䟿å©ã§ã·ã³ãã«ã«ãªããŸã-ããŠã³ããã®ã¯ããèªç¶ãªããšã§ãã
ããããå
·äœçã«ã¯ããã®ã«ãŒã«ãããã«ç ŽãããŸã ååšããªãæ°ããã³ã³ããŒãã³ãã®ã€ã³ã¿ãŒãã§ã€ã¹ãšã³ã³ãã©ã¯ãã決å®ããã®ã«åœ¹ç«ã€åäœãã¹ããäœæããããšæããŸãã ã³ã³ãããŒã©ãŒã¯ãç¹å®ã®ã³ã³ããŒãã³ãXããçæãããã¢ãã«ãè¿ãå¿
èŠãããããã®ãã¹ããäœæããŸããã
@ExtendWith(MockitoExtension.class) class IndexControllerTest { @Mock CakeFinder cakeFinder; @InjectMocks IndexController indexController; private Set<Cake> cakes = Set.of(new Cake("Test 1", "£10"), new Cake("Test 2", "£10")); @BeforeEach void setUp() { when(cakeFinder.findPromotedCakes()).thenReturn(cakes); } @Test void shouldReturnAListOfFoundPromotedCakes() { ModelAndView index = indexController.index(); assertThat(index.getModel()).extracting("cakes").contains(cakes); } }
ããã¯çŽç²ãªåäœãã¹ãã§ã-ããã«ã¯ã³ã³ããã¹ããããŒã¿ããŒã¹ããããŸãããMokitoå°çšã§ãã ãŸãããã®ãã¹ãã¯ãSpringãåäœãã¹ãã«ã©ã®ããã«åœ¹ç«ã€ãã瀺ãåªãããã¢ã³ã¹ãã¬ãŒã·ã§ã³ã§ããSpringMVCã®ã³ã³ãããŒã©ãŒã¯ãã¡ãœãããéåžžã®åã®ãã©ã¡ãŒã¿ãŒãåãåããPOJOãªããžã§ã¯ããè¿ãã¯ã©ã¹ã§ã ã HTTPãªã¯ãšã¹ããã¬ã¹ãã³ã¹ãããããŒãJSONãXMLã¯ãããŸããããããã¯ãã¹ãŠãã³ã³ããŒã¿ãŒãšã·ãªã¢ã©ã€ã¶ãŒã®åœ¢åŒã§èªåçã«ã¹ã¿ãã¯ã«é©çšãããŸãã ã¯ããSpringã«ã¯ModelAndView
ã®åœ¢åŒã®ModelAndView
ãªããã³ããããããŸãããããã¯éåžžã®POJOã§ãããå¿
èŠã«å¿ããŠModelAndView
ããããšãã§ããŸãModelAndView
ã³ã³ãããŒã©ãŒã«ç¹ã«å¿
èŠã§ãã
Mockitoã«ã€ããŠã¯ããŸã觊ããŸãããå
¬åŒããã¥ã¡ã³ãã§ãã¹ãŠãèªãããšãã§ããŸãã å
·äœçã«ã¯ããã®ãã¹ãã«ã¯èå³æ·±ãç¹ã®ã¿ããããŸãMockitoExtension.class
ããã¹ãMockitoExtension.class
ãšããŠäœ¿çšãããšã @Mock
ã«ãã£ãŠæ³šéãä»ãããããã£ãŒã«ãã®mokaãèªåçã«çæããã @Mock
ãšããŒã¯ããããã£ãŒã«ãã®ãªããžã§ã¯ãã®ã³ã³ã¹ãã©ã¯ã¿ãŒã«ãããã®mokaãäŸåé¢ä¿ãšããŠæ¿å
¥ãã@InjectMocks
ã Mockito.mock()
ã¡ãœããã䜿çšããŠããããã¹ãŠãæåã§è¡ããã¯ã©ã¹ãäœæã§ããŸãã
ãŸãããã®ãã¹ãã¯ãæ°ããã³ã³ããŒãã³ãã®ã¡ãœããfindPromotedCakes
ãã¡ã€ã³ããŒãžã«è¡šç€ºããã±ãŒãã®ãªã¹ãã®æ±ºå®ã«åœ¹ç«ã¡ãŸãã 圌ã¯ããããäœã§ãããããŸãã¯ããŒã¿ããŒã¹ã§ã©ã®ããã«æ©èœãããã決å®ããŸããã ã³ã³ãããŒã©ãŒã®å¯äžã®è²¬ä»»ã¯ã転éããããã®ãåãåããç¹å®ã®ãã£ãŒã«ãã§ã¢ãã«ïŒãã±ãŒããïŒãè¿ãããšã§ãã ããã«ããããããã CakeFinder
ãã§ã«ç§ã®ã€ã³ã¿ãŒãã§ã€ã¹ã«æåã®ã¡ãœããããããŸããã€ãŸããçµ±åãã¹ããæžãããšãã§ããŸãã
cakes
ããã±ãŒãžå
ã®ãã¹ãŠã®ã¯ã©ã¹ãæ
æã«ãã©ã€ããŒãã«ããŠãããã±ãŒãžå€ã®èª°ã䜿çšã§ããªãããã«ããŸããã ããŒã¿ããŒã¹ããããŒã¿ãååŸããå¯äžã®æ¹æ³ã¯ãCakeFinderã䜿çšããããšã§ããããã¯ãããŒã¿ããŒã¹ã«ã¢ã¯ã»ã¹ããããã®ç§ã®ãã³ã³ããŒãã³ãXãã§ãã ããã¯èªç¶ãªãã³ãã¯ã¿ãã«ãªããŸããäœããåé¢ããŠãã¹ãããããŒã¹ã«è§Šããªãããã«ããå¿
èŠãããå Žåã¯ãç°¡åã«ããã¯ãªã³ã§ããŸãã ãããŠãå¯äžã®å®è£
ã¯JpaCakeFinderã§ãã ãŸããããšãã°ãå°æ¥ãããŒã¿ããŒã¹ãŸãã¯ããŒã¿ãœãŒã¹ã®ã¿ã€ããå€æŽãããå Žåã䜿çšããã³ãŒããå€æŽããã«CakeFinder
ã€ã³ã¿ãŒãã§ãŒã¹ã®å®è£
ãè¿œå ããå¿
èŠããããŸãã
@DataJpaTestã䜿çšããJPAã®çµ±åãã¹ã
çµ±åãã¹ãã¯Spring bread and butterã§ãã å®éãçµ±åãã¹ãã§ã¯ãã¹ãŠãéåžžã«ããŸãè¡ãããŠãããããéçºè
ã¯ãŠãããã¬ãã«ã«ç§»åããããUIã¬ãã«ãç¡èŠãããããããªãå ŽåããããŸãã ããã¯æªãããšã§ãè¯ãããšã§ããããŸããããã¹ãã®äž»ãªç®æšã¯èªä¿¡ã§ããããšãç¹°ãè¿ããŸãã ãããŠããã®ä¿¡é Œæ§ãæäŸããã«ã¯ãè¿
éã§å¹æçãªçµ±åãã¹ãã®ã»ããã§ååãããããŸããã ãã ããæéã®çµéãšãšãã«ããããã®ãã¹ããé
ããªã£ããé
ããªã£ãããçµ±åã§ã¯ãªãã³ã³ããŒãã³ãã®ãã¹ããéå§ãããããå±éºããããŸãã
çµ±åãã¹ãã§ã¯ãã¢ããªã±ãŒã·ã§ã³ããã®ãŸãŸå®è¡ãããïŒ @SpringBootTest
ïŒããŸãã¯ãã®åå¥ã®ã³ã³ããŒãã³ãïŒJPAãWebïŒãå®è¡ã§ããŸãã ç§ã®å ŽåãJPAã«çŠç¹ãåœãŠããã¹ããäœæãããã®ã§ãã³ã³ãããŒã©ãŒãä»ã®ã³ã³ããŒãã³ããæ§æããå¿
èŠã¯ãããŸããã @DataJpaTest
ã¢ãããŒã·ã§ã³ã¯ãSpring Boot Testã§ãããæ
åœããŸãã ããã¯ã¡ã¿æ³šéã§ããã€ãŸãã ãã¹ãã®ããŸããŸãªåŽé¢ãæ§æããããã€ãã®ç°ãªã泚éãçµã¿åãããŸãã
- @AutoConfigureDataJpa
- @AutoConfigureTestDatabase
- @AutoConfigureCache
- @AutoConfigureTestEntityManager
- @Transactional
ãŸããããããã«ã€ããŠåå¥ã«èª¬æãã次ã«ãå®æãããã¹ãã瀺ããŸãã
@AutoConfigureDataJpa
æ§æã®ã»ããå
šäœãããŒããããªããžããªïŒ CrudRepositories
ã®å®è£
ã®èªåçæïŒãFlyWayããã³LiquibaseããŒã¿ããŒã¹ã®ç§»è¡ããŒã«ãDataSourceã䜿çšããããŒã¿ããŒã¹ãžã®æ¥ç¶ããã©ã³ã¶ã¯ã·ã§ã³ãããŒãžã£ãŒãæåŸã«Hibernateãæ§æããŸãã å®éãããŒã¿ãžã®ã¢ã¯ã»ã¹ã«é¢é£ããèšå®ã®ã»ããã«ãããŸããDispatcherServlet
ãä»ã®ã³ã³ããŒãã³ããããã«ã¯å«ãŸããŠããŸããã
@AutoConfigureTestDatabase
ããã¯ãJPAãã¹ãã®æãèå³æ·±ãåŽé¢ã®1ã€ã§ãã ãã®æ§æã¯ãã¯ã©ã¹ãã¹ã§ãµããŒããããŠããçµã¿èŸŒã¿ããŒã¿ããŒã¹ã®1ã€ãæ€çŽ¢ããããŒã¿ãœãŒã¹ãã©ã³ãã ã«äœæãããã¡ã¢ãªå
ããŒã¿ããŒã¹ãæãããã«ã³ã³ããã¹ããåæ§æããŸã ã H2ããŒã¹ã«äŸåé¢ä¿ãè¿œå ããã®ã§ãä»ã«äœãããå¿
èŠã¯ãããŸããããã¹ãã®å®è¡ããšã«ãã®ã¢ãããŒã·ã§ã³ãèªåçã«æã€ã ãã§ã空ã®ããŒã¹ãæäŸãããããã¯éåžžã«äŸ¿å©ã§ãã
ãã®ããŒã¹ã¯ã¹ããŒã ãªãã§å®å
šã«ç©ºã«ãªãããšãèŠããŠãã䟡å€ããããŸãã åè·¯ãçæããã«ã¯ãããã€ãã®ãªãã·ã§ã³ããããŸãã
- Hibernateã®èªåDDLæ©èœã䜿çšããŸãã Spring Boot Testã¯ãã®å€ã
create-drop
èªåçã«èšå®ããHibernateããšã³ãã£ãã£ã®èª¬æããã¹ããŒããçæããã»ãã·ã§ã³ã®çµäºæã«ã¹ããŒããåé€ããããã«ããŸãã ããã¯ããã¹ãã«éåžžã«åœ¹ç«ã€Hibernateã®éåžžã«åŒ·åãªæ©èœã§ãã - FlywayãŸãã¯Liquibaseã«ãã£ãŠäœæããã移è¡ã䜿çšããŸã ã
ããŒã¿ããŒã¹ã§ããŒã¿ããŒã¹ãåæåããããŸããŸãªã¢ãããŒãã«ã€ããŠè©³ããã¯ã ããã¥ã¡ã³ããåç
§ããŠãã ãã ã
@AutoConfigureCache
NoOpCacheManagerã䜿çšããããã«ãã£ãã·ã¥ãæ§æããã ãã§ã-ã€ãŸã äœããã£ãã·ã¥ããªãã§ãã ããã ããã¯ããã¹ãã§ã®é©ããé¿ããã®ã«åœ¹ç«ã¡ãŸãã
@AutoConfigureTestEntityManager
ç¹å¥ãªTestEntityManager
ãªããžã§ã¯ããTestEntityManager
ã«è¿œå ããŸããããèªäœã¯èå³æ·±ãç£ã§ãã EntityManager
ã¯JPAã®ã¡ã€ã³ã¯ã©ã¹ã§ãããã»ãã·ã§ã³ãžã®ãšã³ãã£ãã£ã®è¿œå ãåé€ãªã©ã®åŠçãè¡ããŸãã ããšãã°ãHibernateãåäœãå§ãããšãã«ã®ã¿ããšã³ãã£ãã£ãã»ãã·ã§ã³ã«è¿œå ããŠãããŒã¿ããŒã¹ãžã®ãªã¯ãšã¹ããå®è¡ãããããšãæå³ãããã»ãã·ã§ã³ããã®ããŒãã¯éžæãªã¯ãšã¹ããå®è¡ãããããšãæå³ããŸããã Hibernateã®å
éšã¡ã«ããºã ã«ãããããŒã¿ããŒã¹ã®å®éã®æäœã¯é©åãªã¿ã€ãã³ã°ã§å®è¡ããããã¬ãŒã ã¯ãŒã¯èªäœã決å®ããŸãã ãã ãããã¹ãã®ç®çã¯çµ±åããã¹ãããããšã§ããããããã¹ãã§ã¯ãããŒã¿ããŒã¹ã«äœãã匷å¶çã«éä¿¡ããå¿
èŠãããå ŽåããããŸãã ãŸãã TestEntityManager
ã¯ãããŒã¿ããŒã¹ã®äžéšã®æäœã匷å¶çã«å®è¡ããããã®åãªããã«ããŒã§ããããšãã°ã persistAndFlush()
ã¯ãHibernateã«ãã¹ãŠã®èŠæ±ã匷å¶çã«å®è¡ãããŸãã
@Transactional
ãã®æ³šéã«ãããã¯ã©ã¹å
ã®ãã¹ãŠã®ãã¹ãããã©ã³ã¶ã¯ã·ã§ã³ã«ãªãããã¹ãã®å®äºæã«ãã©ã³ã¶ã¯ã·ã§ã³ãèªåçã«ããŒã«ããã¯ãããŸãã ããã¯ãåãã¹ãã®åã«ããŒã¿ããŒã¹ããã¯ãªãŒãã³ã°ãããããã®ã¡ã«ããºã ã«ãããŸãããããããªããšãåããŒãã«ããããŒã¿ãæåã§åé€ããå¿
èŠããããŸãã
ãã¹ãã§ãã©ã³ã¶ã¯ã·ã§ã³ã管çãããã©ããã¯ãèŠããã»ã©åçŽã§æçœãªåé¡ã§ã¯ãããŸããã ããŒã¿ããŒã¹ã®ãã¯ãªãŒã³ãç¶æ
ã®äŸ¿å©ãã«ããããããããããã«ãã³ãŒãããã©ã³ã¶ã¯ã·ã§ã³èªäœãéå§ãããæ¢åã®ãã®ãå¿
èŠãšããå Žåããã¹ãã§ã®@Transactional
ã®ååšã¯äžå¿«ãªé©ãã§ãã ããã¯çµ±åãã¹ãã«åæ Œãããšããäºå®ã«ã€ãªããå¯èœæ§ããããŸãããå®éã®ã³ãŒãããã¹ãã§ã¯ãªãã³ã³ãããŒã©ãŒããå®è¡ããããšããµãŒãã¹ã«ã¯ã¢ã¯ãã£ããªãã©ã³ã¶ã¯ã·ã§ã³ããªããã¡ãœããã¯äŸå€ãã¹ããŒããŸãã ããã¯å±éºã«èŠããŸãããUIãã¹ãã®é«ã¬ãã«ã®ãã¹ãã§ã¯ããã©ã³ã¶ã¯ã·ã§ã³ãã¹ãã¯ããã»ã©æªããããŸããã ç§ã®çµéšã§ã¯ãçµ±åãã¹ãã«åæ Œãããšå®åã³ãŒããã¯ã©ãã·ã¥ããæ¢åã®ãã©ã³ã¶ã¯ã·ã§ã³ãæããã«å¿
èŠã«ãªã£ããšãã«ãäžåºŠããèŠãŸããã§ããã ãã ãããµãŒãã¹ãšã³ã³ããŒãã³ãèªäœããã©ã³ã¶ã¯ã·ã§ã³ãæ£ãã管çããŠããããšã確èªããå¿
èŠãããå Žåã¯ãç®çã®ã¢ãŒãã§ãã¹ãã®@Transactional
ã¢ãããŒã·ã§ã³ãããããã¯ãã§ããŸãïŒããšãã°ããã©ã³ã¶ã¯ã·ã§ã³ãéå§ããªãïŒã
@SpringBootTestãšã®çµ±åãã¹ã
ãŸãã @DataJpaTest
ã¯ãã©ãŒã«ã«çµ±åãã¹ãã®ãŠããŒã¯ãªäŸã§ã¯ãªãã @WebMvcTest
ã @DataMongoTest
ãªã©å€ãã®ãã®ãããããšã«æ³šæããŠãã ããã ãã ããæãéèŠãªãã¹ãã¢ãããŒã·ã§ã³ã®1ã€ã¯@SpringBootTest
ãŸãŸã§ããã¹ãçšã«ã¢ããªã±ãŒã·ã§ã³ãããã®ãŸãŸãèµ·åããŸã-æ§æããããã¹ãŠã®ã³ã³ããŒãã³ããšçµ±åã䜿çšããŸãã è«ççãªåé¡ãçºçããŸã-ã¢ããªã±ãŒã·ã§ã³å
šäœãå®è¡ã§ããå Žåãããšãã°ããªãDataJpaã®çŠç¹ãã¹ããè¡ãã®ã§ããããïŒ ããã§ãå³æ Œãªã«ãŒã«ã¯ãããŸããã
æ¯åã¢ããªã±ãŒã·ã§ã³ãå®è¡ãããã¹ãã§ã¯ã©ãã·ã¥ãåãåããéè² è·ã«ãªããããã¹ãã®ã»ããã¢ãããè€éã«ããªãããšãå¯èœãªå Žå㯠ããã¡ãã@SpringBootTestã䜿çšã§ããŸãã
ãã ããå®éã«ã¯ãã¢ããªã±ãŒã·ã§ã³ã¯å€ãã®ç°ãªãèšå®ãå¿
èŠãšããç°ãªãã·ã¹ãã ã«æ¥ç¶ããå¯èœæ§ããããããŒã¿ããŒã¹ã¢ã¯ã»ã¹ãã¹ãã倱æããããšã¯æãŸãããããŸããã ã¡ãã»ãŒãžãã¥ãŒãžã®æ¥ç¶ã¯èšå®ãããŠããŸããã ãããã£ãŠãåžžèã䜿çšããããšãéèŠã§ãã@ SpringBootTestã¢ãããŒã·ã§ã³ã䜿çšããŠãã¹ããæ©èœãããã«ã¯ãã·ã¹ãã ã®ååãããã¯ããå¿
èŠããããŸãã@ SpringBootTestã§ããã¯ãŸã£ããæå³ããããŸããïŒ
ãã¹ãçšããŒã¿ã®æºå
ãã¹ãã®éèŠãªãã€ã³ãã®1ã€ã¯ãããŒã¿ã®æºåã§ãã åãã¹ãã¯åé¢ããŠå®è¡ããéå§ããåã«ç°å¢ãæºåããŠãã·ã¹ãã ãå
ã®æãŸããç¶æ
ã«ããå¿
èŠããããŸãã ãããè¡ãæãç°¡åãªãªãã·ã§ã³ã¯ã @BeforeEach
/ @BeforeAll
ã¢ãããŒã·ã§ã³ã䜿çšãããªããžããªã EntityManager
ãŸãã¯TestEntityManager
ã䜿çšããŠããŒã¿ããŒã¹ã«ãšã³ããªãè¿œå ããããšTestEntityManager
ã ãã ããæºåæžã¿ã®ã¹ã¯ãªãããå®è¡ããããç®çã®SQLã¯ãšãªãå®è¡ãããã§ããå¥ã®ãªãã·ã§ã³ããããŸããããã¯@Sql
ã¢ãããŒã·ã§ã³ã§ãã ãã¹ããå®è¡ããåã«ãSpring Boot Testã¯æå®ãããã¹ã¯ãªãããèªåçã«å®è¡ãã @BeforeAll
ãããã¯ãè¿œå ããå¿
èŠããªããªãã @BeforeAll
ãããŒã¿@Transactional
ãåŠçã@Transactional
ã
@DataJpaTest class JpaCakeFinderTest { private static final String PROMOTED_CAKE = "Red Velvet"; private static final String NON_PROMOTED_CAKE = "Victoria Sponge"; private CakeFinder finder; @Autowired CakeRepository cakeRepository; @Autowired TestEntityManager testEntityManager; @BeforeEach void setUp() { this.testEntityManager.persistAndFlush(CakeEntity.builder().title(PROMOTED_CAKE) .sku("SKU1").price(BigDecimal.TEN).promoted(true).build()); this.testEntityManager.persistAndFlush(CakeEntity.builder().sku("SKU2") .title(NON_PROMOTED_CAKE).price(BigDecimal.ONE).promoted(false).build()); finder = new JpaCakeFinder(cakeRepository); } ... }
èµ€ç·ãªãã¡ã¯ã¿ãªã³ã°ãµã€ã¯ã«
éçºè
ã«ãšã£ãŠããã®éã®ããã¹ãã«ããããããããã¹ãã¯@DataJpaTestã¢ãããŒã·ã§ã³ã䜿çšããåçŽãªã¯ã©ã¹ã®ããã«èŠããŸãããéçºè
ãèããããšã®ã§ããªãå
éšã§ã©ãã»ã©æçšãªããšãèµ·ãã£ãŠãããã瀺ãããšãã§ãããšæããŸãã ããã§TDDãµã€ã¯ã«ã«é²ãããšãã§ããŸããä»åã¯ããªãã¡ã¯ã¿ãªã³ã°ãšæå°éã®ã³ãŒãã®äŸãšãšãã«ãTDDã®å埩ãããã€ã瀺ããŸãã ããããããããããã«ãGitã®å±¥æŽã確èªããããšã匷ããå§ãããŸããGitã§ã¯ãåã³ãããã¯ããããäœãã©ã®ããã«è¡ãã®ãã説æããå¥åã®éèŠãªã¹ãããã§ãã
ããŒã¿æºå
@BeforeAll
/ @BeforeEach
ã¢ãããŒãã䜿çšããããŒã¿ããŒã¹å
ã®ãã¹ãŠã®ã¬ã³ãŒããæåã§äœæããŸãã @Sql
ã¢ãããŒã·ã§ã³ã䜿çšããäŸã¯ãå¥ã®ã¯ã©ã¹JpaCakeFinderTestWithScriptSetup
移åããŸãããã¹ããè€è£œããŸãããã¡ããããã¹ãã¯è€è£œãããã¹ãã§ã¯ãªããã¢ãããŒãã瀺ãããã ãã«ååšããŸãã
ã·ã¹ãã ã®åæç¶æ
-ã·ã¹ãã ã«ã¯2ã€ã®ãšã³ããªãããã1ã€ã®ã±ãŒããããã¢ãŒã·ã§ã³ã«åå ããã¡ãœããã«ãã£ãŠè¿ãããçµæã«å«ãŸããå¿
èŠããããŸãã2ã€ç®-ãããã
æåã®ãã¹ãçµ±åãã¹ã
æåã®ãã¹ãã¯æãç°¡åã§ãfindPromotedCakes
ã¯ãããã¢ãŒã·ã§ã³ã«åå ããŠããã±ãŒãã®èª¬æãšäŸ¡æ Œãå«ããå¿
èŠããããŸãã
èµ€
@Test void shouldReturnPromotedCakes() { Iterable<Cake> promotedCakes = finder.findPromotedCakes(); assertThat(promotedCakes).extracting(Cake::getTitle).contains(PROMOTED_CAKE); assertThat(promotedCakes).extracting(Cake::getPrice).contains("£10.00"); }
ãã¡ããããã¹ãã¯ã¯ã©ãã·ã¥ããŸã-ããã©ã«ãã®å®è£
ã¯ç©ºã®Setãè¿ããŸãã
ç·è²
åœç¶ãããã«ãã£ã«ã¿ãªã³ã°ãèšè¿°ãwhere
ããŒã¿ããŒã¹ããªã¯ãšã¹ãããã®where
ãªã©ãå¿
èŠã«ãªããŸãã ããããTDDã®å®è·µã«åŸã£ãŠããã¹ããåæ Œããããã®æå°éã®ã³ãŒããèšè¿°ããå¿
èŠããããŸã ã ãããŠããã®æå°éã®ã³ãŒãã¯ãããŒã¿ããŒã¹å
ã®ãã¹ãŠã®ã¬ã³ãŒããè¿ãããšã§ãã ã¯ãããšãŠãã·ã³ãã«ã§é³è
ã§ãã
public Set<Cake> findPromotedCakes() { Spliterator<CakeEntity> cakes = this.cakeRepository.findAll() .spliterator(); return StreamSupport.stream(cakes, false).map( cakeEntity -> new Cake(cakeEntity.title, formatPrice(cakeEntity.price))) .collect(Collectors.toSet()); } private String formatPrice(BigDecimal price) { return "£" + price.setScale(2, RoundingMode.DOWN).toPlainString(); }
ãããããããã§ã¯ãããŒã¹ããªããŠããã¹ããã°ãªãŒã³ã«ã§ãããšäž»åŒµããã§ãããããã¹ãã§æåŸ
ãããçµæãããŒãã³ãŒãããã ãã§ãã ç§ã¯ææãã®ãããªè°è«ãè³ã«ããŸãããTDDã¯æ矩ã§ãå®æã§ããªãããšã誰ããç解ããŠãããšæããŸãããããäžæ¡çãªç¹ã«æã¡èŸŒãããšã¯æå³ããããŸããã ããããæ¬åœã«ãããå Žåã¯ãããšãã°ãã€ã³ã¹ããŒã«äžã®ããŒã¿ãã©ã³ãã åããŠãããŒãã³ãŒãã£ã³ã°ãããªãããã«ããããšãã§ããŸãã
ãªãã¡ã¯ã¿ãªã³ã°
ããã§ã¯ããŸããªãã¡ã¯ã¿ãªã³ã°ãè¡ã£ãŠããªãã®ã§ããã®ç¹å®ã®ãã¹ãã§ã¯ãã®ãã§ãŒãºãã¹ãããã§ããŸãã ããããç§ã¯ãŸã ãã®æ®µéãç¡èŠããããšã¯ãå§ãããŸãããã·ã¹ãã ã®ãã°ãªãŒã³ãç¶æ
ã§æ¯ååæ¢ããŠèããããšããå§ãããŸã-äœãããªãã¡ã¯ã¿ãªã³ã°ããŠããè¯ãç°¡åã«ããããšã¯å¯èœã§ããïŒ
äºæ¬¡è©Šéš
ãã ãã2çªç®ã®ãã¹ãã§ã¯ãããã¢ãŒã·ã§ã³ãããŠããªãã±ãŒããfindPromotedCakes
ã«ãã£ãŠè¿ãããçµæã«è©²åœããªãããšãæ¢ã«æ€èšŒããŠããŸãã
@Test void shouldNotReturnNonPromotedCakes() { Iterable<Cake> promotedCakes = finder.findPromotedCakes(); assertThat(promotedCakes).extracting(Cake::getTitle) .doesNotContain(NON_PROMOTED_CAKE); }
èµ€
ãã¹ãã¯äºæ³éãã¯ã©ãã·ã¥ããŸã-ããŒã¿ããŒã¹ã«ã¯2ã€ã®ã¬ã³ãŒãããããã³ãŒãã¯ããããã¹ãŠãåã«è¿ããŸãã
ç·è²
ç¹°ãè¿ãã«ãªããŸããããã¹ãã«åæ Œããããã«èšè¿°ã§ããæå°ã³ãŒãã¯äœã§ããïŒ ã¹ããªãŒã ãšãã®ã¢ã»ã³ããªãæ¢ã«ååšãããããããã«filter
ãããã¯ãè¿œå ããã ãã§ãã
public Set<Cake> findPromotedCakes() { Spliterator<CakeEntity> cakes = this.cakeRepository.findAll() .spliterator(); return StreamSupport.stream(cakes, false) .filter(cakeEntity -> cakeEntity.promoted) .map(cakeEntity -> new Cake(cakeEntity.title, formatPrice(cakeEntity.price))) .collect(Collectors.toSet()); }
ãã¹ããåéããŸã-çµ±åãã¹ãã¯ç·è²ã«ãªããŸããã éèŠãªç¬éãæ¥ãŸãã-ã³ã³ãããŒã©ãŒã®åäœãã¹ããšããŒã¿ããŒã¹ãæäœããããã®çµ±åãã¹ãã®çµã¿åããã«ãããç§ã®æ©èœã®æºåãã§ããŸãã-ãããŠUIãã¹ãã«åæ ŒããŸããïŒ
ãªãã¡ã¯ã¿ãªã³ã°
ãããŠããã¹ãŠã®ãã¹ããç·è²ã§ããããããªãã¡ã¯ã¿ãªã³ã°ã®æéã§ãã ã¡ã¢ãªå
ã§ã®ãã£ã«ã¿ãªã³ã°ã¯è¯ãã¢ã€ãã¢ã§ã¯ãªãããšãæ確ã«ããå¿
èŠã¯ãªããšæããŸããããŒã¿ããŒã¹ã§ãããè¡ãããšããå§ãããŸãã ãããè¡ãããã«ã CakesRepository
æ°ããã¡ãœããfindByPromotedIsTrue
ãè¿œå ããŸããã
interface CakeRepository extends CrudRepository<CakeEntity, String> { Iterable<CakeEntity> findByPromotedIsTrue(); }
ãã®ã¡ãœããã®å ŽåãSpring Dataã¯ã select from cakes where promoted = true
ãã©ãŒã select from cakes where promoted = true
ã¯ãšãªãå®è¡ããã¡ãœãããèªåçã«çæããŸããã ã¯ãšãªçæã®è©³çŽ°ã«ã€ããŠã¯ãSpring Dataã®ããã¥ã¡ã³ããã芧ãã ããã
public Set<Cake> findPromotedCakes() { Spliterator<CakeEntity> cakes = this.cakeRepository.findByPromotedIsTrue() .spliterator(); return StreamSupport.stream(cakes, false).map( cakeEntity -> new Cake(cakeEntity.title, formatPrice(cakeEntity.price))) .collect(Collectors.toSet()); }
ããã¯ãçµ±åãã¹ããšãã©ãã¯ããã¯ã¹ã¢ãããŒããæäŸããæè»æ§ã®è¯ãäŸã§ãã ãªããžããªãããã¯ãããŠããå Žåããã¹ããå€æŽããã«æ°ããã¡ãœãããè¿œå ããããšã¯äžå¯èœã§ã¯ãããŸããã§ããã
çç£æ ç¹ãžã®æ¥ç¶
ããªã¢ãªãºã ããå°ãè¿œå ãããã¹ããšã¡ã€ã³ã¢ããªã±ãŒã·ã§ã³ã®æ§æãåé¢ããæ¹æ³ã瀺ãããã«ããå®çšŒåãã¢ããªã±ãŒã·ã§ã³ã®ããŒã¿ã¢ã¯ã»ã¹æ§æãè¿œå ããŸãã
äŒçµ±çã«ã¯ãã¹ãŠapplication.yml
ã®ã»ã¯ã·ã§ã³ã«ãã£ãŠè¿œå ããapplication.yml
ïŒ
datasource: url: jdbc:h2:./data/cake-factory
ããã«ããããã¡ã€ã«ã·ã¹ãã ã®ããŒã¿ã./data
ãã©ã«ããŒã«èªåçã«ä¿åãããŸãã ãã®ãã©ã«ããŒã¯ãã¹ãã§ã¯äœæãããªãããšã«æ³šæããŠãã ãã- @DataJpaTest
ã¯ã @AutoConfigureTestDatabase
ã¢ãããŒã·ã§ã³ã®ååšã«ããããã¡ã€ã«ããŒã¿ããŒã¹ãžã®æ¥ç¶ãã¡ã¢ãªå
ã®ã©ã³ãã ããŒã¿ããŒã¹ã«èªåçã«çœ®ãæããŸãã
䟿å©ã«ãªãå¯èœæ§ããã2ã€ã®äŸ¿å©ãªãã®ã¯ã data.sql
schema.sql
ãšschema.sql
ãã¢ããªã±ãŒã·ã§ã³ãèµ·åãããšãSpring Bootã¯ãªãœãŒã¹å
ã«ãããã®ãã¡ã€ã«ãååšãããã©ããã確èªããååšããå Žåã¯ãããã®ã¹ã¯ãªãããå®è¡ããŸãããã®æ©èœã¯ãå®éã®ããŒã¿ããŒã¹ã§ã®ããŒã«ã«éçºããã³ãããã¿ã€ãã³ã°ã«åœ¹ç«ã¡ãŸãããã¡ããã移è¡ããŒã«ã䜿çšããå¿
èŠããããŸãã
ãããã«
ãããã£ãŠãä»åã¯ããã¹ãã䜿çšããŠããŒã¿ã«ã¢ã¯ã»ã¹ããããã®ãµãŒãã¹ã®ã€ã³ã¿ãŒãã§ã€ã¹ã決å®ããæ¹æ³ãçµ±åãã¹ããäœæããæ¹æ³ãããã³TDDãµã€ã¯ã«ã§æå°éã®ã³ãŒããäœæããæ¹æ³ã瀺ããŸããã
次ã®èšäºã§ã¯ãSpring Securityãè¿œå ããŸã-ããŸããŸãªãŠãŒã¶ãŒãšããŒã«ã«å¯ŸããŠã¢ããªã±ãŒã·ã§ã³ããã¹ãããæ¹æ³ãšãSpringãããã«æäŸããããŒã«ãšããã¹ãã®å¢çã決å®ããæ¹æ³ã瀺ããŸãã