วันนี้ผมอยากจะพูดถึงการทดสอบ เพราะยิ่งโค้ดครอบคลุมการทดสอบมากเท่าไรก็ยิ่งถือว่าดีและเชื่อถือได้มากขึ้นเท่านั้น เราจะไม่พูดถึงการทดสอบหน่วย แต่เกี่ยวกับการทดสอบการรวมฐานข้อมูล ความแตกต่างระหว่างการทดสอบหน่วยและการทดสอบการรวมคืออะไร? โมดูลาร์ (หน่วย) คือการทดสอบโปรแกรมในระดับของแต่ละโมดูล วิธีการ หรือคลาส กล่าวคือ การทดสอบนั้นรวดเร็วและง่ายดาย ซึ่งส่งผลต่อส่วนที่แบ่งแยกได้มากที่สุดของฟังก์ชันการทำงาน เรียกอีกอย่างว่า "การทดสอบหนึ่งรายการต่อวิธี" การบูรณาการจะช้ากว่าและหนักกว่า และอาจประกอบด้วยหลายโมดูลและฟังก์ชันเพิ่มเติม เหตุใดการทดสอบสำหรับการทดสอบการรวมเลเยอร์ dao (Data Access Object) จึงเป็นเช่นนั้น เนื่องจากในการทดสอบวิธีที่มีการสืบค้นไปยังฐานข้อมูล เราจำเป็นต้องสร้างฐานข้อมูลแยกต่างหากใน RAM โดยแทนที่ฐานข้อมูลหลัก แนวคิดก็คือเราสร้างตารางที่เราต้องการ กรอกข้อมูลการทดสอบและตรวจสอบความถูกต้องของวิธีการคลาสพื้นที่เก็บข้อมูล (ท้ายที่สุดแล้ว เรารู้ว่าผลลัพธ์สุดท้ายควรเป็นอย่างไรในกรณีที่กำหนด) เอาล่ะ มาเริ่มกันเลย หัวข้อเกี่ยวกับการเชื่อมต่อฐานข้อมูลได้รับการกล่าวถึงมานานแล้วดังนั้นวันนี้ฉันไม่อยากพูดถึงเรื่องนี้และเราจะพิจารณาเฉพาะส่วนของโปรแกรมที่เราสนใจเท่านั้น ตามค่าเริ่มต้น เราจะเริ่มจากข้อเท็จจริงที่ว่าแอปพลิเคชันของเราใช้ Spring Boot สำหรับเลเยอร์ Spring JDBC dao (เพื่อความชัดเจนยิ่งขึ้น) ฐานข้อมูลหลักของเราคือ MySQL และเราจะแทนที่โดยใช้ MariaDB (เข้ากันได้สูงสุด และ ดังนั้นสคริปต์ MySQL จะไม่ไม่มีความขัดแย้งกับภาษา MariaDB เนื่องจากจะมีกับ H2) นอกจากนี้เรายังจะถือว่ามีเงื่อนไขว่าโปรแกรมของเราใช้ Liquibase เพื่อจัดการและใช้การเปลี่ยนแปลงกับสคีมาฐานข้อมูล ดังนั้นสคริปต์ที่ใช้ทั้งหมดจะถูกเก็บไว้ในแอปพลิเคชันของเรา
โครงสร้างโครงการ
แสดงเฉพาะส่วนที่ได้รับผลกระทบ: และใช่ วันนี้เราจะสร้างโรบอต)) สคริปต์สำหรับตาราง วิธีที่เราจะทดสอบวันนี้ (create_table_robots.sql):CREATE TABLE `robots`
(
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`name` CHAR(255) CHARACTER SET utf8 NOT NULL,
`cpu` CHAR(255) CHARACTER SET utf8 NOT NULL,
`producer` CHAR(255) CHARACTER SET utf8 NOT NULL,
PRIMARY KEY (`id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
เอนทิตีที่เป็นตัวแทนของตารางนี้:
@Builder
@Data
public class Robot {
private Long id;
private String name;
private String cpu;
private String producer;
}
อินเทอร์เฟซสำหรับพื้นที่เก็บข้อมูลที่ทดสอบ:
public interface RobotDAO {
Robot findById(Long id);
Robot create(Robot robot);
List<Robot> findAll();
Robot update(Robot robot);
void delete(Long id);
}
ที่จริงแล้วนี่คือการดำเนินการ CRUD มาตรฐานโดยไม่มีสิ่งแปลกใหม่ ดังนั้นเราจะพิจารณาการใช้งานไม่ใช่ทุกวิธี (ซึ่งจะไม่ทำให้ใครแปลกใจ) แต่บางวิธี - เพื่อความกระชับยิ่งขึ้น:
@Repository
@AllArgsConstructor
public class RobotDAOImpl implements RobotDAO {
private static final String FIND_BY_ID = "SELECT id, name, cpu, producer FROM robots WHERE id = ?";
private static final String UPDATE_BY_ID = "UPDATE robots SET name = ?, cpu = ?, producer = ? WHERE id = ?";
@Autowired
private final JdbcTemplate jdbcTemplate;
@Override
public Robot findById(Long id) {
return jdbcTemplate.queryForObject(FIND_BY_ID, robotMapper(), id);
}
@Override
public Robot update(Robot robot) {
jdbcTemplate.update(UPDATE_BY_ID,
robot.getName(),
robot.getCpu(),
robot.getProducer(),
robot.getId());
return robot;
}
private RowMapper<Robot> robotMapper() {
return (rs, rowNum) ->
Robot.builder()
.id(rs.getLong("id"))
.name(rs.getString("name"))
.cpu(rs.getString("cpu"))
.producer(rs.getString("producer"))
.build();
}
ลองพูดนอกเรื่องเล็กน้อยและดูว่าเกิดอะไรขึ้นกับการพึ่งพาของเรา (เฉพาะส่วนที่ใช้สำหรับส่วนที่สาธิตของแอปพลิเคชันเท่านั้นที่จะถูกนำเสนอ):
<dependencies>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<version>2.5.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.craftercms.mariaDB4j</groupId>
<artifactId>mariaDB4j-springboot</artifactId>
<version>2.4.2.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.2.1.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
</dependencies>
4 - การพึ่งพาฐานข้อมูล MariaDb เอง 10 - การพึ่งพาสำหรับการเชื่อมต่อกับ SpringBoot 16 - ลอมบอก (ฉันคิดว่าทุกคนรู้ว่านี่คือ lib ประเภทใด) 22 - การเริ่มต้นสำหรับการทดสอบ (โดยที่ JUnit ที่เราต้องการถูกฝังอยู่) 28 - การเริ่มต้นสำหรับ การทำงานกับ springJdbc มาดู Spring Container พร้อมถั่วที่จำเป็นสำหรับการทดสอบของเรากัน (โดยเฉพาะ MariaDB Creation Bean):
@Configuration
public class TestConfigDB {
@Bean
public MariaDB4jSpringService mariaDB4jSpringService() {
return new MariaDB4jSpringService();
}
@Bean
public DataSource dataSource(MariaDB4jSpringService mariaDB4jSpringService) {
try {
mariaDB4jSpringService.getDB().createDB("testDB");
} catch (ManagedProcessException e) {
e.printStackTrace();
}
DBConfigurationBuilder config = mariaDB4jSpringService.getConfiguration();
return DataSourceBuilder
.create()
.username("root")
.password("root")
.url(config.getURL("testDB"))
.driverClassName("org.mariadb.jdbc.Driver")
.build();
}
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
5 - องค์ประกอบหลักสำหรับการเพิ่ม MariaDB (สำหรับแอปพลิเคชันที่ใช้ Spring Framework) 10 - การกำหนด bean ฐานข้อมูล 12 - การตั้งชื่อฐานข้อมูลที่สร้างขึ้น 17 - ดึงการกำหนดค่าสำหรับกรณีของเราออกมา 19 - การสร้างฐานข้อมูลโดยใช้รูปแบบ Builder ( ภาพรวมที่ดีของรูปแบบ ) และสุดท้าย สิ่งที่ยุ่งยากทั้งหมดคือ JdbcTemplate bean สำหรับการสื่อสารกับฐานข้อมูลที่กำลังถูกยกขึ้น แนวคิดก็คือ เราจะมีคลาสหลักสำหรับการทดสอบ Tao ซึ่งคลาสการทดสอบ Dao ทั้งหมดจะสืบทอดมา ซึ่งมีงานดังนี้:
- การเรียกใช้สคริปต์บางตัวที่ใช้ในฐานข้อมูลหลัก (สคริปต์สำหรับสร้างตาราง การเปลี่ยนคอลัมน์ และอื่นๆ)
- การเปิดตัวสคริปต์ทดสอบที่เติมตารางด้วยข้อมูลการทดสอบ
- กำลังลบตาราง
@SpringBootTest(classes = TestConfigDB.class)
public abstract class DataBaseIT {
@Autowired
private JdbcTemplate jdbcTemplate;
public JdbcTemplate getJdbcTemplate() {
return jdbcTemplate;
}
public void fillDataBase(String[] initList) {
for (String x : initList) {
try {
jdbcTemplate.update(IOUtils.resourceToString("/db.migrations/" + x, StandardCharsets.UTF_8));
} catch (IOException e) {
e.printStackTrace();
}
}
}
public void cleanDataBase() {
getJdbcTemplate().update("DROP database testDB");
getJdbcTemplate().update("CREATE database testDB");
getJdbcTemplate().update("USE testDB");
}
public void fillTables(String[] fillList) {
for (String x : fillList) {
try {
Stream.of(
IOUtils.resourceToString("/fill_scripts/" + x, StandardCharsets.UTF_8))
.forEach(jdbcTemplate::update);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
1 - การใช้คำอธิบายประกอบ @SpringBootTest เราตั้งค่าการกำหนดค่าการทดสอบ 11 - เป็นอาร์กิวเมนต์ในวิธีนี้เราส่งชื่อของตารางที่เราต้องการ และเขาในฐานะผู้ทำงานหนักที่มีความรับผิดชอบจะโหลดตารางเหล่านั้นให้เรา (ซึ่งทำให้เรามีโอกาส เพื่อนำวิธีนี้กลับมาใช้ใหม่ได้มากเท่าที่ใจเราต้องการ) 21 - เราใช้วิธีนี้ในการทำความสะอาด ได้แก่ การลบตารางทั้งหมด (และข้อมูล) ออกจากฐานข้อมูล 27 - อาร์กิวเมนต์ในวิธีนี้คืออาร์เรย์ของชื่อสคริปต์พร้อมข้อมูลทดสอบ ที่จะโหลดเพื่อทดสอบวิธีการเฉพาะ สคริปต์ของเรา พร้อมข้อมูลการทดสอบ:
INSERT INTO robots(name, cpu, producer)
VALUES ('Rex', 'Intel Core i5-9400F', 'Vietnam'),
('Molly', 'AMD Ryzen 7 2700X', 'China'),
('Ross', 'Intel Core i7-9700K', 'Malaysia')
และตอนนี้สิ่งที่เราได้รวบรวมมาเพื่อวันนี้
ชั้นเรียนทดสอบเต๋า
@RunWith(SpringRunner.class)
public class RobotDataBaseIT extends DataBaseIT {
private static RobotDAO countryDAO;
@Before
public void fillData() {
fillDataBase(new String[]{
"create_table_robots.sql"
});
countryDAO = new RobotDAOImpl(getJdbcTemplate());
}
@After
public void clean() {
cleanDataBase();
}
private RowMapper<Robot> robotMapper() {
return (rs, rowNum) ->
Robot.builder()
.id(rs.getLong("id"))
.name(rs.getString("name"))
.cpu(rs.getString("cpu"))
.producer(rs.getString("producer"))
.build();
}
2 - เราสืบทอดมาจากคลาสหลักสำหรับการทดสอบของเรา 4 - พื้นที่เก็บข้อมูลทดสอบของเรา 7 - วิธีการที่จะเปิดใช้งานก่อนการทดสอบแต่ละครั้ง 8 - เราใช้วิธีคลาสหลักเพื่อโหลดตารางที่จำเป็น 11 - เราเริ่มต้น dao 15 ของเรา - วิธีการ ที่จะเปิดตัวหลังการทดสอบแต่ละครั้ง การล้างฐานข้อมูลของเรา 19 - การใช้งาน RowMapper ของเรา ซึ่งคล้ายคลึงกับคลาส Tao เราใช้ @Before และ @After ซึ่งใช้ก่อนและหลังวิธีทดสอบวิธีหนึ่ง แต่เราสามารถนำ lib บางส่วนที่ช่วยให้เรา เพื่อใช้คำอธิบายประกอบที่เชื่อมโยงกับจุดเริ่มต้นของการทดสอบการดำเนินการของคลาสนี้และจุดสิ้นสุด ตัวอย่างเช่นอันนี้ซึ่งจะเร่งการทดสอบได้อย่างมาก เนื่องจากจะต้องสร้างและลบตารางทั้งหมดทุกครั้ง และหนึ่งครั้งต่อชั้นเรียน แต่เราไม่ทำอย่างนั้น คุณถามทำไม? จะเกิดอะไรขึ้นหากวิธีใดวิธีหนึ่งเปลี่ยนโครงสร้างของตาราง? ตัวอย่างเช่น ลบหนึ่งคอลัมน์ ในกรณีนี้ วิธีการที่เหลืออาจล้มเหลวหรือต้องตอบสนองอย่างเหมาะสม (เช่น สร้างคอลัมน์ด้านหลัง) เราต้องยอมรับว่าสิ่งนี้ทำให้เราเชื่อมโยงกันโดยไม่จำเป็น (การพึ่งพา) การทดสอบซึ่งกันและกัน ซึ่งไม่มีประโยชน์สำหรับเรา แต่ฉันเชือนแชไปดำเนินการต่อ ...
ทดสอบเมธอด findById
@Test
public void findByIdTest() {
fillTables(new String[]{"fill_table_robots.sql"});
Long id = getJdbcTemplate().queryForObject("SELECT id FROM robots WHERE name = 'Molly'", Long.class);
Robot robot = countryDAO.findById(id);
assertThat(robot).isNotNull();
assertThat(robot.getId()).isEqualTo(id);
assertThat(robot.getName()).isEqualTo("Molly");
assertThat(robot.getCpu()).isEqualTo("AMD Ryzen 7 2700X");
assertThat(robot.getProducer()).isEqualTo("China");
}
3 - กรอกข้อมูลการทดสอบลงในตาราง 5 - รับรหัสสำหรับเอนทิตีที่เราต้องการ 6 - ใช้วิธีการทดสอบ 8...12 - เปรียบเทียบข้อมูลที่ได้รับกับข้อมูลที่คาดหวัง
อัปเดตวิธีทดสอบ
@Test
public void updateTest() {
fillTables(new String[]{"fill_table_robots.sql"});
Long robotId = getJdbcTemplate().queryForObject("SELECT id FROM robots WHERE name = 'Rex'", Long.class);
Robot updateRobot = Robot.builder()
.id(robotId)
.name("Aslan")
.cpu("Intel Core i5-3470")
.producer("Narnia")
.build();
Robot responseRobot = countryDAO.update(updateRobot);
Robot updatedRobot = getJdbcTemplate().queryForObject(
"SELECT id, name, cpu, producer FROM robots WHERE id = ?",
robotMapper(),
robotId);
assertThat(updatedRobot).isNotNull();
assertThat(updateRobot.getName()).isEqualTo(responseRobot.getName());
assertThat(updateRobot.getName()).isEqualTo(updatedRobot.getName());
assertThat(updateRobot.getCpu()).isEqualTo(responseRobot.getCpu());
assertThat(updateRobot.getCpu()).isEqualTo(updatedRobot.getCpu());
assertThat(updateRobot.getProducer()).isEqualTo(responseRobot.getProducer());
assertThat(updateRobot.getProducer()).isEqualTo(updatedRobot.getProducer());
assertThat(responseRobot.getId()).isEqualTo(updatedRobot.getId());
assertThat(updateRobot.getId()).isEqualTo(updatedRobot.getId());
}
3 - กรอกตารางด้วยข้อมูลการทดสอบ 5 - รับรหัสของเอนทิตีที่กำลังอัปเดต 7 - สร้างเอนทิตีที่อัปเดต 14 - ใช้วิธีการทดสอบ 15 - รับเอนทิตีที่อัปเดตสำหรับการตรวจสอบ 20...28 - เปรียบเทียบข้อมูลที่ได้รับกับ สิ่งที่คาดหวัง การทดสอบวิธีการอัพเดตนั้นคล้ายกับการสร้าง อย่างน้อยสำหรับฉัน คุณสามารถบิดการกระทบยอดได้มากเท่าที่คุณต้องการ โดยจะต้องไม่มีเช็คมากเกินไป ฉันอยากจะทราบด้วยว่าการทดสอบไม่รับประกันการทำงานเต็มรูปแบบหรือไม่มีข้อบกพร่อง การทดสอบทำให้แน่ใจได้ว่าผลลัพธ์ที่แท้จริงของโปรแกรม (แฟรกเมนต์ของโปรแกรม) สอดคล้องกับผลลัพธ์ที่คาดไว้เท่านั้น ในกรณีนี้จะมีการตรวจสอบเฉพาะส่วนที่เขียนการทดสอบเท่านั้น
มาเปิดคลาสแบบทดสอบกันดีกว่า...
ชัยชนะ)) ไปชงชาและรับคุกกี้กันเถอะ: เราสมควรได้รับมัน))ลิงค์ที่เป็นประโยชน์
- บทความที่ดีเกี่ยวกับส่วนที่ยังไม่ได้ตรวจสอบของกองเทคโนโลยีนี้
- บทความที่น่าสนใจเกี่ยวกับ Maven, Spring, MySQL, Hibernate
- มารีเฟรชหน่วยความจำของเรากันเล็กน้อยเกี่ยวกับการทดสอบหน่วย
- ตัวอย่างที่น่าสนใจของการทดสอบการรวมระบบ แต่มีการทดแทน PostgreSQL
- หลายๆ คน ไม่เพียงแต่ในการทดสอบเท่านั้นที่ใช้ MariaDB แทน MySql
*เพลง Star Wars อันยิ่งใหญ่*
GO TO FULL VERSION