JavaRush /จาวาบล็อก /Random-TH /Mapstruct คืออะไรและจะกำหนดค่าอย่างไรให้เหมาะสมสำหรับการท...
Roman Beekeeper
ระดับ

Mapstruct คืออะไรและจะกำหนดค่าอย่างไรให้เหมาะสมสำหรับการทดสอบหน่วยในแอปพลิเคชัน SpringBoot

เผยแพร่ในกลุ่ม

พื้นหลัง

สวัสดีทุกคน เพื่อนรักและผู้อ่านของฉัน! ก่อนที่เราจะเขียนบทความ พื้นหลังเล็กน้อย... ฉันเพิ่งพบปัญหาในการทำงานกับ ไลบรารี Mapstructซึ่งฉันได้อธิบายไว้สั้น ๆ ในช่องโทรเลขของฉันที่นี่ ปัญหาเกี่ยวกับโพสต์ได้รับการแก้ไขแล้วในความคิดเห็น เพื่อนร่วมงานของฉันจากโครงการก่อนหน้านี้ช่วยในเรื่องนี้ Mapstruct คืออะไรและจะกำหนดค่าอย่างไรให้เหมาะสมสำหรับการทดสอบหน่วยในแอปพลิเคชัน SpringBoot  ตอนที่ 1 - 1หลังจากนั้น ฉันตัดสินใจเขียนบทความในหัวข้อนี้ แต่แน่นอนว่าเราจะไม่ใช้มุมมองที่แคบ และจะพยายามเร่งความเร็วให้ได้ก่อน ทำความเข้าใจว่า Mapstruct คืออะไร และเหตุใดจึงจำเป็น และเราจะใช้ตัวอย่างจริง วิเคราะห์สถานการณ์ที่เกิดขึ้นก่อนหน้านี้และวิธีแก้ไข ดังนั้นฉันขอแนะนำอย่างยิ่งให้ทำการคำนวณทั้งหมดควบคู่ไปกับการอ่านบทความเพื่อสัมผัสประสบการณ์ทุกอย่างในทางปฏิบัติ ก่อนที่เราจะเริ่ม สมัครสมาชิกช่องโทรเลขของฉันฉันรวบรวมกิจกรรมของฉันที่นั่น เขียนความคิดเกี่ยวกับการพัฒนาใน Java และไอทีโดยทั่วไป สมัครสมาชิก? ยอดเยี่ยม! เอาล่ะไปกันเลย!

Mapstruct คำถามที่พบบ่อย?

ตัวสร้างโค้ดสำหรับการแมป bean ที่รวดเร็วและปลอดภัย ภารกิจแรกของเราคือการหาว่า Mapstruct คืออะไร และเหตุใดเราจึงต้องการมัน โดยทั่วไปคุณสามารถอ่านได้บนเว็บไซต์อย่างเป็นทางการ ในหน้าหลักของเว็บไซต์มีสามคำตอบสำหรับคำถาม: มันคืออะไร? เพื่ออะไร? ยังไง? เราจะพยายามทำสิ่งนี้ด้วย:

มันคืออะไร?

Mapstruct เป็นไลบรารีที่ช่วยในการแมป (แผนที่ โดยทั่วไปนั่นคือสิ่งที่พวกเขามักจะพูด: แผนที่ แผนที่ ฯลฯ ) ออบเจ็กต์ของเอนทิตีบางตัวให้เป็นออบเจ็กต์ของเอนทิตีอื่น ๆ โดยใช้โค้ดที่สร้างขึ้นตามการกำหนดค่าที่อธิบายผ่านอินเทอร์เฟซ

เพื่ออะไร?

โดยส่วนใหญ่ เราพัฒนาแอปพลิเคชันแบบหลายเลเยอร์ (เลเยอร์สำหรับการทำงานกับฐานข้อมูล, เลเยอร์ของตรรกะทางธุรกิจ, เลเยอร์สำหรับการโต้ตอบของแอปพลิเคชันกับโลกภายนอก) และแต่ละเลเยอร์มีออบเจ็กต์ของตัวเองสำหรับจัดเก็บและประมวลผลข้อมูล . และข้อมูลนี้จำเป็นต้องถ่ายโอนจากชั้นหนึ่งไปอีกชั้นหนึ่งโดยการถ่ายโอนจากเอนทิตีหนึ่งไปยังอีกเอนทิตี สำหรับผู้ที่ยังไม่เคยใช้วิธีนี้ อาจดูซับซ้อนเล็กน้อย ตัวอย่างเช่น เรามีเอนทิตีสำหรับฐานข้อมูลนักเรียน เมื่อข้อมูลของเอนทิตีนี้ไปที่เลเยอร์ตรรกะทางธุรกิจ (บริการ) เราจำเป็นต้องถ่ายโอนข้อมูลจากคลาส Student ไปยังคลาส StudentModel ถัดไป หลังจากจัดการกับตรรกะทางธุรกิจแล้ว ข้อมูลจะต้องถูกเผยแพร่ออกไปภายนอก และสำหรับสิ่งนี้ เรามีคลาส StudentDto แน่นอนว่าเราต้องส่งข้อมูลจากคลาส StudentModel ไปยัง StudentDto การเขียนด้วยมือแต่ละครั้งวิธีการที่จะถ่ายทอดต้องใช้แรงงานเข้มข้น นอกจากนี้นี่คือโค้ดพิเศษในฐานโค้ดที่ต้องดูแลรักษา คุณสามารถทำผิดพลาดได้ และ Mapstruct จะสร้างวิธีการดังกล่าวในขั้นตอนการคอมไพล์และจัดเก็บไว้ในแหล่งที่สร้างขึ้น

ยังไง?

การใช้คำอธิบายประกอบ เราเพียงแค่ต้องสร้างคำอธิบายประกอบที่มีคำอธิบายประกอบ Mapper หลักที่บอกไลบรารีว่าวิธีการในอินเทอร์เฟซนี้สามารถใช้เพื่อแปลจากวัตถุหนึ่งไปยังอีกวัตถุหนึ่งได้ ดังที่ได้กล่าวไว้ก่อนหน้านี้เกี่ยวกับนักเรียน ในกรณีของเรา นี่จะเป็นอินเทอร์เฟซ StudentMapper ซึ่งจะมีหลายวิธีในการถ่ายโอนข้อมูลจากเลเยอร์หนึ่งไปยังอีกเลเยอร์หนึ่ง:
public class Student {
   private Long id;
   private String firstName;
   private String lastName;
   private Integer age;
}

public class StudentDTO {
   private Long id;
   private String firstName;
   private String lastName;
   private Integer age;
}

public class StudentModel {
   private Long id;
   private String firstName;
   private String lastName;
   private Integer age;
}
สำหรับคลาสเหล่านี้ เราสร้าง mapper (ต่อไปนี้คือสิ่งที่เราจะเรียกว่าอินเทอร์เฟซ ซึ่งอธิบายสิ่งที่เราต้องการถ่ายโอนและตำแหน่ง):
@Mapper
public interface StudentMapper {
   StudentModel toModel(StudentDTO dto);
   Student toEntity(StudentModel model);
   StudentModel toModel(Student entity);
   StudentDTO toDto(StudentModel model);
}
ข้อดีของแนวทางนี้คือหากชื่อและประเภทของฟิลด์เหมือนกันในคลาสที่แตกต่างกัน (เช่นในกรณีของเรา) การตั้งค่าสำหรับ Mapstruct ก็เพียงพอที่จะสร้างการใช้งานที่จำเป็นตามอินเทอร์เฟซ StudentMapper ในขั้นตอนการคอมไพล์ ซึ่ง จะแปล มันก็ชัดเจนขึ้นแล้วใช่ไหม? มาดูตัวอย่างจริงเพื่อวิเคราะห์งานในแอปพลิเคชัน Spring Boot กันดีกว่า

ตัวอย่างการทำงานของ Spring Boot และ Mapstruct

สิ่งแรกที่เราต้องการคือสร้างโปรเจ็กต์ Spring Boot และเพิ่ม Mapstruct เข้าไป สำหรับเรื่องนี้ ฉันมีองค์กรใน GitHubที่มีเทมเพลตสำหรับพื้นที่เก็บข้อมูล และการเริ่มต้นสำหรับ Spring Boot ก็เป็นหนึ่งในนั้น จากนั้นเราจึงสร้างโปรเจ็กต์ใหม่: Mapstruct คืออะไรและจะกำหนดค่าอย่างไรให้เหมาะสมสำหรับการทดสอบหน่วยในแอปพลิเคชัน SpringBoot  ส่วนที่ 1 - 2ต่อไปเราจะได้โปรเจ็กต์ ใช่เพื่อน ๆ ให้ดาวแก่โครงการถ้าคุณพบว่ามีประโยชน์ เพื่อฉันจะรู้ว่าฉันไม่ได้ทำสิ่งนี้โดยเปล่าประโยชน์ ในโครงการนี้ เราจะเปิดเผยสถานการณ์ที่ฉันได้รับในที่ทำงานและอธิบายไว้ในโพสต์ในช่อง Telegram ของฉัน ฉันจะสรุปสถานการณ์คร่าวๆ สำหรับผู้ที่ไม่ทราบ: เมื่อเราเขียนการทดสอบสำหรับผู้ทำแผนที่ (นั่นคือ สำหรับการใช้งานอินเทอร์เฟซที่เราพูดถึงก่อนหน้านี้) เราต้องการให้การทดสอบผ่านไปโดยเร็วที่สุด ตัวเลือกที่ง่ายที่สุดสำหรับตัวทำแผนที่คือการใช้คำอธิบายประกอบ SpringBootTest เมื่อรันการทดสอบ ซึ่งจะรับ ApplicationContext ทั้งหมดของแอปพลิเคชัน Spring Boot และฉีดตัวทำแผนที่ที่จำเป็นสำหรับการทดสอบภายในการทดสอบ แต่ตัวเลือกนี้ใช้ทรัพยากรมากและใช้เวลานานกว่ามากจึงไม่เหมาะกับเรา เราจำเป็นต้องเขียน Unit Test ที่สร้าง Mapper ที่ต้องการและตรวจสอบว่าวิธีการของมันทำงานตรงตามที่เราคาดหวังหรือไม่ เหตุใดคุณจึงต้องมีการทดสอบเพื่อให้ทำงานเร็วขึ้น หากการทดสอบใช้เวลานาน กระบวนการพัฒนาทั้งหมดจะช้าลง จนกว่าการทดสอบจะส่งต่อโค้ดใหม่ รหัสนี้จะไม่สามารถถือว่าถูกต้องและจะไม่ถูกนำไปทดสอบ ซึ่งหมายความว่าจะไม่ถูกนำไปใช้จริง และหมายความว่านักพัฒนายังทำงานไม่เสร็จ ดูเหมือนว่าทำไมต้องเขียนแบบทดสอบห้องสมุดที่การดำเนินงานไม่มีข้อสงสัย? แต่เรายังจำเป็นต้องเขียนการทดสอบ เนื่องจากเรากำลังทดสอบว่าเราอธิบายผู้ทำแผนที่ได้ถูกต้องเพียงใด และเป็นไปตามที่เราคาดหวังหรือไม่ ก่อนอื่น เพื่อให้งานของเราง่ายขึ้น มาเพิ่ม Lombok เข้ากับโปรเจ็กต์ของเราโดยเพิ่มการอ้างอิงอื่นให้กับ pom.xml:
<dependency>
   <groupId>org.projectlombok</groupId>
   <artifactId>lombok</artifactId>
   <scope>provided</scope>
</dependency>
ในโครงการของเรา เราจะต้องถ่ายโอนจากคลาสโมเดล (ซึ่งใช้ในการทำงานกับตรรกะทางธุรกิจ) ไปยังคลาส DTO ซึ่งเราใช้ในการสื่อสารกับโลกภายนอก ในเวอร์ชันที่เรียบง่ายของเรา เราจะถือว่าฟิลด์ไม่มีการเปลี่ยนแปลงและผู้ทำแผนที่ของเราจะเรียบง่าย แต่หากมีความต้องการ ก็สามารถเขียนบทความที่มีรายละเอียดเพิ่มเติมเกี่ยวกับวิธีการทำงานกับ Mapstruct วิธีกำหนดค่า และวิธีการใช้ประโยชน์จากมัน แต่เนื่องจากบทความนี้จะค่อนข้างยาว สมมติว่าเรามีนักเรียนที่มีรายชื่ออาจารย์และอาจารย์ที่เขาเข้าร่วม มาสร้าง แพ็คเกจโมเดลกัน จากนี้ เราจะสร้างแบบจำลองง่ายๆ:
package com.github.romankh3.templaterepository.springboot.dto;

import lombok.Data;

import java.util.List;

@Data
public class StudentDTO {

   private Long id;

   private String name;

   private List<LectureDTO> lectures;

   private List<LecturerDTO> lecturers;
}
การบรรยายของเขา
package com.github.romankh3.templaterepository.springboot.dto;

import lombok.Data;

@Data
public class LectureDTO {

   private Long id;

   private String name;
}
และวิทยากร
package com.github.romankh3.templaterepository.springboot.dto;

import lombok.Data;

@Data
public class LecturerDTO {

   private Long id;

   private String name;
}
และสร้าง แพ็คเกจ dtoถัดจากmodel package :
package com.github.romankh3.templaterepository.springboot.dto;

import lombok.Data;

import java.util.List;

@Data
public class StudentDTO {

   private Long id;

   private String name;

   private List<LectureDTO> lectures;

   private List<LecturerDTO> lecturers;
}
การบรรยาย
package com.github.romankh3.templaterepository.springboot.dto;

import lombok.Data;

@Data
public class LectureDTO {

   private Long id;

   private String name;
}
และวิทยากร
package com.github.romankh3.templaterepository.springboot.dto;

import lombok.Data;

@Data
public class LecturerDTO {

   private Long id;

   private String name;
}
ตอนนี้เรามาสร้าง Mapper ที่จะแปลคอลเลกชันแบบจำลองการบรรยายเป็นคอลเลกชันของการบรรยาย DTO สิ่งแรกที่ต้องทำคือเพิ่ม Mapstruct ให้กับโปรเจ็กต์ ในการดำเนินการนี้ เราจะใช้เว็บไซต์อย่างเป็นทางการซึ่งมีคำอธิบายทุกอย่างไว้ที่นั่น นั่นคือเราจำเป็นต้องเพิ่มการพึ่งพาหนึ่งรายการและปลั๊กอินในหน่วยความจำของเรา (หากคุณมีคำถามเกี่ยวกับหน่วยความจำคืออะไร ไปเลยArticle1และArticle2 ):
<dependency>
   <groupId>org.mapstruct</groupId>
   <artifactId>mapstruct</artifactId>
   <version>1.4.2.Final</version>
</dependency>
และในหน่วยความจำในบล็อก <build/> ซึ่งเรายังไม่มี:
<build>
   <plugins>
       <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-compiler-plugin</artifactId>
           <version>3.5.1</version>
           <configuration>
               <source>1.8</source>
               <target>1.8</target>
               <annotationProcessorPaths>
                   <path>
                       <groupId>org.mapstruct</groupId>
                       <artifactId>mapstruct-processor</artifactId>
                       <version>1.4.2.Final</version>
                   </path>
               </annotationProcessorPaths>
           </configuration>
       </plugin>
   </plugins>
</build>
ต่อไป มาสร้างแพ็คเกจmapperถัดจากdtoและmodel จากคลาสที่เราแสดงไปก่อนหน้านี้ คุณจะต้องสร้างผู้ทำแผนที่เพิ่มอีกห้าคน:
  • Mapper LectureModel <-> LectureDTO
  • รายการผู้ทำแผนที่<LectureModel> <-> รายการ<LectureDTO>
  • Mapper อาจารย์โมเดล <-> อาจารย์สทท
  • รายการผู้ทำแผนที่<LecturerModel> <-> รายการ<LecturerDTO>
  • Mapper StudentModel <-> StudentDTO
ไป:

การบรรยายMapper

package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.dto.LecturerDTO;
import com.github.romankh3.templaterepository.springboot.model.LectureModel;
import org.mapstruct.Mapper;

@Mapper(componentModel = "spring")
public interface LectureMapper {
   LectureDTO toDTO(LectureModel model);

   LectureModel toModel(LecturerDTO dto);
}

LectureListMapper

package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.model.LectureModel;
import org.mapstruct.Mapper;

import java.util.List;

@Mapper(componentModel = "spring", uses = LectureMapper.class)
public interface LectureListMapper {
   List<LectureModel> toModelList(List<LectureDTO> dtos);
   List<LectureDTO> toDTOList(List<LectureModel> models);
}

อาจารย์แมปเปอร์

package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.model.LectureModel;
import org.mapstruct.Mapper;

@Mapper(componentModel = "spring")
public interface LectureMapper {
   LectureDTO toDTO(LectureModel model);

   LectureModel toModel(LectureDTO dto);
}

อาจารย์ListMapper

package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LecturerDTO;
import com.github.romankh3.templaterepository.springboot.model.LecturerModel;
import org.mapstruct.Mapper;

import java.util.List;

@Mapper(componentModel = "spring", uses = LecturerMapper.class)
public interface LecturerListMapper {
   List<LecturerModel> toModelList(List<LecturerDTO> dloList);
   List<LecturerDTO> toDTOList(List<LecturerModel> modelList);
}

StudentMapper

package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.StudentDTO;
import com.github.romankh3.templaterepository.springboot.model.StudentModel;
import org.mapstruct.Mapper;

@Mapper(componentModel = "spring", uses = {LectureListMapper.class, LecturerListMapper.class})
public interface StudentMapper {
   StudentDTO toDTO(StudentModel model);
   StudentModel toModel(StudentDTO dto);
}
ควรสังเกตแยกกันว่าในตัวสร้างแผนที่ เราหมายถึงผู้ทำแผนที่คนอื่นๆ สิ่งนี้ทำได้ผ่าน ฟิลด์ การใช้งานในคำอธิบายประกอบ Mapper เช่นเดียวกับที่ทำใน StudentMapper:
@Mapper(componentModel = "spring", uses = {LectureListMapper.class, LecturerListMapper.class})
ที่นี่เราใช้ผู้ทำแผนที่สองคนเพื่อจับคู่รายชื่อการบรรยายและรายชื่ออาจารย์อย่างถูกต้อง ตอนนี้เราจำเป็นต้องรวบรวมโค้ดของเราและดูว่ามีอะไรและอย่างไร ซึ่งสามารถทำได้โดยใช้ คำสั่ง mvn cleanคอมไพล์ แต่ปรากฏว่าเมื่อสร้างการใช้งาน Mapstruct ของผู้ทำแผนที่ของเรา การใช้งานผู้ทำแผนที่ไม่ได้เขียนทับฟิลด์ ทำไม ปรากฎว่าไม่สามารถรับคำอธิบายประกอบข้อมูลจากลอมบอกได้ และต้องทำอะไรบางอย่าง... ดังนั้นเราจึงมีหัวข้อใหม่ในบทความ

การเชื่อมโยงลอมบอกและ Mapstruct

หลังจากค้นหาไม่กี่นาที ปรากฏว่าเราต้องเชื่อมต่อลอมบอกและ Mapstruct ด้วยวิธีใดวิธีหนึ่ง มีข้อมูลเกี่ยวกับสิ่งนี้ในเอกสาร Mapstruct หลังจากตรวจสอบตัวอย่างที่เสนอโดยนักพัฒนาจาก Mapstruct แล้ว มาอัปเดต pom.xml ของเรากันดีกว่า: มาเพิ่มเวอร์ชันแยกกัน:
​​<properties>
   <org.mapstruct.version>1.4.2.Final</org.mapstruct.version>
   <lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
</properties>
มาเพิ่มการพึ่งพาที่ขาดหายไป:
<dependency>
   <groupId>org.projectlombok</groupId>
   <artifactId>lombok-mapstruct-binding</artifactId>
   <version>${lombok-mapstruct-binding.version}</version>
</dependency>
และมาอัปเดตปลั๊กอินคอมไพเลอร์ของเราเพื่อให้สามารถเชื่อมต่อลอมบอกและ Mapstruct:
<plugin>
   <groupId>org.apache.maven.plugins</groupId>
   <artifactId>maven-compiler-plugin</artifactId>
   <version>3.5.1</version>
   <configuration>
       <source>1.8</source>
       <target>1.8</target>
       <annotationProcessorPaths>
           <path>
               <groupId>org.mapstruct</groupId>
               <artifactId>mapstruct-processor</artifactId>
               <version>${org.mapstruct.version}</version>
           </path>
           <path>
               <groupId>org.projectlombok</groupId>
               <artifactId>lombok</artifactId>
               <version>${lombok.version}</version>
           </path>
           <path>
               <groupId>org.projectlombok</groupId>
               <artifactId>lombok-mapstruct-binding</artifactId>
               <version>${lombok-mapstruct-binding.version}</version>
           </path>
       </annotationProcessorPaths>
   </configuration>
</plugin>
หลังจากนี้ทุกอย่างควรจะได้ผล มารวบรวมโครงการของเราอีกครั้ง แต่คุณจะพบคลาสที่ Mapstruct สร้างขึ้นได้ที่ไหน? พวกเขาอยู่ในแหล่งที่มาที่สร้างขึ้น: ${projectDir}/target/generated-sources/annotations/ Mapstruct คืออะไรและจะกำหนดค่าอย่างไรให้เหมาะสมสำหรับการทดสอบหน่วยในแอปพลิเคชัน SpringBoot  ส่วนที่ 1 - 3ตอนนี้เราพร้อมที่จะตระหนักถึงความผิดหวังของฉันจากโพสต์ Mapstruct แล้ว เรามาลองสร้างการทดสอบสำหรับผู้ทำแผนที่กันดีกว่า

เราเขียนการทดสอบสำหรับผู้ทำแผนที่ของเรา

ฉันจะสร้างการทดสอบที่ง่ายและรวดเร็วซึ่งจะทดสอบหนึ่งในผู้ทำแผนที่ ในกรณีที่เรากำลังสร้างการทดสอบการรวมและไม่ต้องกังวลกับเวลาดำเนินการ:

แบบทดสอบการบรรยายแมปเปอร์

package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.model.LectureModel;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class LectureMapperTest {

   @Autowired
   private LectureMapper mapperUnderTest;

   @Test
   void shouldProperlyMapModelToDto() {
       //given
       LectureModel model = new LectureModel();
       model.setId(11L);
       model.setName("lecture name");

       //when
       LectureDTO dto = mapperUnderTest.toDTO(model);

       //then
       Assertions.assertNotNull(dto);
       Assertions.assertEquals(model.getId(), dto.getId());
       Assertions.assertEquals(model.getName(), dto.getName());
   }

   @Test
   void shouldProperlyMapDtoToModel() {
       //given
       LectureDTO dto = new LectureDTO();
       dto.setId(11L);
       dto.setName("lecture name");

       //when
       LectureModel model = mapperUnderTest.toModel(dto);

       //then
       Assertions.assertNotNull(model);
       Assertions.assertEquals(dto.getId(), model.getId());
       Assertions.assertEquals(dto.getName(), model.getName());
   }
}
ที่นี่โดยใช้คำอธิบายประกอบ SpringBootTest เราเปิดตัว applicationContext ทั้งหมด และจากนั้นโดยใช้คำอธิบายประกอบ Autowired เราแยกคลาสที่เราต้องการสำหรับการทดสอบ ในแง่ของความรวดเร็วและความสะดวกในการเขียนแบบทดสอบถือว่าดีมาก การทดสอบผ่านไปด้วยดี ทุกอย่างเรียบร้อยดี แต่เราจะไปอีกทางหนึ่งและเขียน Unit Test สำหรับผู้ทำแผนที่ เช่น LectureListMapper...
package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.model.LectureModel;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.util.Collections;
import java.util.List;

class LectureListMapperTest {

   private final LectureListMapper lectureListMapper = new LectureListMapperImpl();

   @Test
   void shouldProperlyMapListDtosToListModels() {
       //given
       LectureDTO dto = new LectureDTO();
       dto.setId(12L);
       dto.setName("I'm BATMAN!");

       List<LectureDTO> dtos = Collections.singletonList(dto);

       //when
       List<LectureModel> models = lectureListMapper.toModelList(dtos);

       //then
       Assertions.assertNotNull(models);
       Assertions.assertEquals(1, models.size());
       Assertions.assertEquals(dto.getId(), models.get(0).getId());
       Assertions.assertEquals(dto.getName(), models.get(0).getName());
   }
}
เนื่องจากการใช้งานที่ Mapstruct สร้างขึ้นนั้นอยู่ในระดับเดียวกันกับโปรเจ็กต์ของเรา เราจึงสามารถใช้พวกมันในการทดสอบของเราได้อย่างง่ายดาย ทุกอย่างดูดีมาก ไม่มีคำอธิบายประกอบ เราสร้างคลาสที่เราต้องการด้วยวิธีที่ง่ายที่สุด เท่านี้ก็เรียบร้อย แต่เมื่อเรารันการทดสอบ เราจะเข้าใจว่ามันจะพัง และจะมี NullPointerException ในคอนโซล... นี่เป็นเพราะว่าการใช้งานแมปเปอร์ LectureListMapper มีลักษณะดังนี้:
package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.model.LectureModel;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Generated;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Generated(
   value = "org.mapstruct.ap.MappingProcessor",
   date = "2021-12-09T21:46:12+0300",
   comments = "version: 1.4.2.Final, compiler: javac, environment: Java 15.0.2 (N/A)"
)
@Component
public class LectureListMapperImpl implements LectureListMapper {

   @Autowired
   private LectureMapper lectureMapper;

   @Override
   public List<LectureModel> toModelList(List<LectureDTO> dtos) {
       if ( dtos == null ) {
           return null;
       }

       List<LectureModel> list = new ArrayList<LectureModel>( dtos.size() );
       for ( LectureDTO lectureDTO : dtos ) {
           list.add( lectureMapper.toModel( lectureDTO ) );
       }

       return list;
   }

   @Override
   public List<LectureDTO> toDTOList(List<LectureModel> models) {
       if ( models == null ) {
           return null;
       }

       List<LectureDTO> list = new ArrayList<LectureDTO>( models.size() );
       for ( LectureModel lectureModel : models ) {
           list.add( lectureMapper.toDTO( lectureModel ) );
       }

       return list;
   }
}
หากเราดูที่ NPE (ย่อมาจาก NullPointerException) เราจะได้มาจาก ตัวแปร LectureMapperซึ่งกลายเป็นว่าไม่ได้เตรียมใช้งาน แต่ในการใช้งานของเรา เราไม่มีตัวสร้างที่เราสามารถเริ่มต้นตัวแปรได้ นี่คือเหตุผลว่าทำไม Mapstruct ถึงใช้ mapper ด้วยวิธีนี้! ใน Spring คุณสามารถเพิ่ม bean ลงในคลาสได้หลายวิธี คุณสามารถแทรกถั่วผ่านฟิลด์พร้อมกับคำอธิบายประกอบแบบ Autowired ดังที่ทำข้างต้น หรือคุณสามารถฉีดถั่วผ่านตัวสร้างก็ได้ ฉันพบว่าตัวเองตกอยู่ในสถานการณ์ที่มีปัญหาในที่ทำงานเมื่อฉันต้องการปรับเวลาดำเนินการทดสอบให้เหมาะสม ฉันคิดว่าคงทำอะไรไม่ได้และระบายความเจ็บปวดของฉันลงในช่อง Telegram ของฉัน จากนั้นพวกเขาก็ช่วยฉันในการแสดงความคิดเห็นและบอกว่าสามารถปรับแต่งกลยุทธ์การฉีดได้ อินเท อร์เฟซ Mapper มี ฟิลด์ การฉีด Strategyซึ่งเพิ่งยอมรับ ชื่อ ฉีดซึ่งมีสองค่า: FIELDและCONSTRUCTOR เมื่อรู้สิ่งนี้แล้ว เรามาเพิ่มการตั้งค่านี้ให้กับผู้ทำแผนที่กันดีกว่า ฉันจะแสดงโดยใช้LectureListMapper เป็นตัวอย่าง :
@Mapper(componentModel = "spring", uses = LectureMapper.class, injectionStrategy = InjectionStrategy.CONSTRUCTOR)
public interface LectureListMapper {
   List<LectureModel> toModelList(List<LectureDTO> dtos);
   List<LectureDTO> toDTOList(List<LectureModel> models);
}
ฉันเน้นส่วนที่ฉันเพิ่มด้วยตัวหนา มาเพิ่มตัวเลือกนี้สำหรับตัวเลือกอื่นๆ ทั้งหมดและคอมไพล์โปรเจ็กต์ใหม่เพื่อให้ผู้ทำแผนที่ถูกสร้างขึ้นด้วยบรรทัดใหม่ เมื่อเสร็จแล้ว เรามาดูกันว่าการใช้งาน mapper สำหรับLectureListMapper มีการเปลี่ยนแปลงอย่างไร (เน้นส่วนที่เราต้องการด้วยตัวหนา):
package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.model.LectureModel;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Generated;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Generated(
   value = "org.mapstruct.ap.MappingProcessor",
   date = "2021-12-09T22:25:37+0300",
   comments = "version: 1.4.2.Final, compiler: javac, environment: Java 15.0.2 (N/A)"
)
@Component
public class LectureListMapperImpl implements LectureListMapper {

   private final LectureMapper lectureMapper;

   @Autowired
   public LectureListMapperImpl(LectureMapper lectureMapper) {

       this.lectureMapper = lectureMapper;
   }

   @Override
   public List<LectureModel> toModelList(List<LectureDTO> dtos) {
       if ( dtos == null ) {
           return null;
       }

       List<LectureModel> list = new ArrayList<LectureModel>( dtos.size() );
       for ( LectureDTO lectureDTO : dtos ) {
           list.add( lectureMapper.toModel( lectureDTO ) );
       }

       return list;
   }

   @Override
   public List<LectureDTO> toDTOList(List<LectureModel> models) {
       if ( models == null ) {
           return null;
       }

       List<LectureDTO> list = new ArrayList<LectureDTO>( models.size() );
       for ( LectureModel lectureModel : models ) {
           list.add( lectureMapper.toDTO( lectureModel ) );
       }

       return list;
   }
}
และตอนนี้ Mapstruct ได้ดำเนินการ mapper injection ผ่าน Constructor แล้ว นี่คือสิ่งที่เราพยายามทำให้สำเร็จ ตอนนี้การทดสอบของเราจะหยุดการคอมไพล์ มาอัปเดตและรับ:
package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.model.LectureModel;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.util.Collections;
import java.util.List;

class LectureListMapperTest {

   private final LectureListMapper lectureListMapper = new LectureListMapperImpl(new LectureMapperImpl());

   @Test
   void shouldProperlyMapListDtosToListModels() {
       //given
       LectureDTO dto = new LectureDTO();
       dto.setId(12L);
       dto.setName("I'm BATMAN!");

       List<LectureDTO> dtos = Collections.singletonList(dto);

       //when
       List<LectureModel> models = lectureListMapper.toModelList(dtos);

       //then
       Assertions.assertNotNull(models);
       Assertions.assertEquals(1, models.size());
       Assertions.assertEquals(dto.getId(), models.get(0).getId());
       Assertions.assertEquals(dto.getName(), models.get(0).getName());
   }
}
ทีนี้ ถ้าเรารันการทดสอบ ทุกอย่างจะทำงานตามที่คาดไว้ เนื่องจากใน LectureListMapperImpl เราผ่าน LectureMapper ที่ต้องการ... ชัยชนะ! ไม่ใช่เรื่องยากสำหรับคุณ แต่ฉันยินดี: เพื่อน ๆ ทุกอย่างเป็นปกติ สมัครสมาชิกบัญชี GitHub ของฉัน ไปยังบัญชี Telegram ของ ฉัน ฉันโพสต์ผลลัพธ์กิจกรรมของฉันที่นั่นมีประโยชน์จริงๆ) ฉันขอเชิญคุณเข้าร่วมกลุ่มสนทนาของช่องโทรเลข เป็นพิเศษ มันเกิดขึ้นว่าหากใครมีคำถามทางเทคนิค พวกเขาสามารถได้รับคำตอบที่นั่น รูปแบบนี้น่าสนใจสำหรับทุกคน คุณสามารถอ่านใครจะรู้อะไรและรับประสบการณ์ได้

บทสรุป

ในส่วนหนึ่งของบทความนี้ เราได้ทำความคุ้นเคยกับผลิตภัณฑ์ที่จำเป็นและใช้บ่อยเช่น Mapstruct เราพบว่ามันคืออะไร ทำไม และอย่างไร จากตัวอย่างจริง เรารู้สึกว่าสิ่งใดสามารถทำได้และจะเปลี่ยนแปลงได้อย่างไร นอกจากนี้เรายังดูวิธีตั้งค่าการฉีด bean ผ่าน Constructor เพื่อให้สามารถทดสอบ Mappers ได้อย่างเหมาะสม เพื่อนร่วมงานจาก Mapstruct อนุญาตให้ผู้ใช้ผลิตภัณฑ์ของตนเลือกวิธีการแทรกผู้ทำแผนที่ได้อย่างแน่นอน ซึ่งเราต้องขอบคุณพวกเขาอย่างไม่ต้องสงสัย แต่ถึงแม้ว่า Spring จะแนะนำให้ฉีด bean ผ่าน Constructor แต่คนจาก Mapstruct ก็ได้ตั้งค่าการฉีดผ่านสนามเป็นค่าเริ่มต้น ทำไมเป็นอย่างนั้น? ไม่มีคำตอบ. ฉันสงสัยว่าอาจมีเหตุผลที่เราไม่รู้ และนั่นคือสาเหตุที่พวกเขาทำเช่นนี้ และเพื่อหาคำตอบจากพวกเขา ฉันจึงได้สร้างปัญหา GitHubบนคลังผลิตภัณฑ์อย่างเป็นทางการของพวกเขา
ความคิดเห็น
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION