ฉันจะทดสอบคลาสที่ใช้ SimpleJdbcCall ได้อย่างไร

ฉันมีชั้นเรียนที่มีลักษณะเช่นนี้:

public class MyClass {

    private final SimpleJdbcCall simpleJdbcCall;

    public MyClass(final DataSource dataSource) {
        this(new JdbcTemplate(dataSource));
    }

    public MyClass(final JdbcTemplate template) {
        simpleJdbcCall = new SimpleJdbcCall(template)
            .withoutProcedureColumnMetaDataAccess()
            .withCatalogName("MY_ORACLE_PACKAGE")
            .withFunctionName("GET_VALUE")
            .withReturnValue()
            .declareParameters(
                new SqlOutParameter("RESULT", Types.VARCHAR))
            .declareParameters(
                new SqlParameter("P_VAR1_NAME", Types.VARCHAR))
            .declareParameters(
                new SqlParameter("P_VAR2_NAME", Types.VARCHAR))
            .useInParameterNames("P_VAR1_NAME", "P_VAR2_NAME");
    }

    private String getValue(final String input) {
        final SqlParameterSource params = new MapSqlParameterSource()
            .addValue("P_VAR1_NAME", input, Types.VARCHAR)
            .addValue("P_VAR2_NAME", null, Types.VARCHAR);
        return simpleJdbcCall.executeFunction(String.class, params);
    }
}

มันทำงานได้ตามที่คาดไว้ แต่ฉันต้องการเขียนการทดสอบหน่วยและมันทำให้ฉันคลั่งไคล้ ฉันได้ลองเยาะเย้ย JdbcTemplate (Mockito) แล้ว แต่นั่นนำไปสู่การเยาะเย้ยการเชื่อมต่อ ข้อมูลเมตา ฯลฯ และฉันหลงไปกับเวลาที่โรงงานคำสั่งที่สามารถเรียกได้เข้ามามีบทบาท

ฉันเดาว่าฉันสามารถเขียนมันเพื่อให้ SimpleJdbcCall ถูกส่งผ่านเป็นพารามิเตอร์ไปยังตัวสร้างใหม่แล้วจึงเยาะเย้ยสิ่งนั้น แต่นั่นให้ความรู้สึกเหมือนถูกแฮ็ก ฉันอยากให้การทดสอบไม่ส่งผลกระทบต่อชั้นเรียน เว้นแต่เป็นการปรับปรุง

ฉันต้องการใช้ SimpleJdbcCall API นี้ต่อไป มันเขียน SQL ให้ฉันดังนั้นฉันจึงไม่ต้องผสม SQL และ Java แต่ฉันก็อยากทดสอบสิ่งนี้โดยไม่ต้องเขียนโค้ด 1,000 บรรทัด มีใครเห็นวิธีที่ดีในการทดสอบสิ่งนี้หรือไม่


person Matt Malone    schedule 19.10.2015    source แหล่งที่มา


คำตอบ (5)


ฉันก็ไม่ชอบที่จะฉีด SimpleJdbcCalls ที่แตกต่างกัน 15 รายการลงในที่เก็บของฉันเช่นกัน ดังนั้นฉันจึงกัดกระสุนและเพิ่มสิ่งนี้ลงในวิธีการตั้งค่าการทดสอบของฉัน:

DatabaseMetaData metaData = mock(DatabaseMetaData.class);
Connection con = mock(Connection.class);
when(con.getMetaData()).thenReturn(metaData);
DataSource ds = mock(DataSource.class);
when(ds.getConnection()).thenReturn(con);
jdbcTemplate = mock(JdbcTemplate.class);
when(jdbcTemplate.getDataSource()).thenReturn(ds);
person Jeff E    schedule 30.01.2019

ฉันจะใช้วิธีการเพิ่มตัวสร้างเพื่อให้ SimpleJdbcCall ถูกฉีดโดยตรงอย่างแน่นอน

MyClass(SimpleJdbcCall simpleJdbcCall) {
  this.simpleJdbcCall = simpleJdbcCall;
}

(และอาจเรียกใช้ตัวสร้างนั้นจากตัวที่เรียกใช้ new ในปัจจุบัน)

นี่ไม่ใช่ "การแฮ็ก" แต่เป็นเพียง การแทรกการพึ่งพา ฉันขอยืนยันว่าการทำให้ชั้นเรียนทดสอบได้โดยไม่จำเป็นต้องทดสอบการทำงานของ SimpleJdbcCall นั้นเป็นการปรับปรุงที่ชัดเจน

การเรียกใช้ new ใน Constructor ทำให้การทดสอบยากขึ้น เนื่องจากเป็นการมีเพศสัมพันธ์แบบคงที่ที่แน่นหนากับคลาสที่กำลังสร้างอินสแตนซ์

ฉันพบโพสต์บล็อกของ Miško Hevery เกี่ยวกับเรื่องนี้ หัวข้อที่น่าสนใจมาก

person Andy Turner    schedule 19.10.2015
comment
DI มีไว้สำหรับการพึ่งพาที่กำหนดค่าได้จริง ๆ และไม่เหมาะที่จะแทนที่ออบเจ็กต์ stateful เช่น SimpleJdbcCall และการขึ้นต่อกันเช่นนี้สามารถล้อเลียนได้ ดังนั้นโค้ดจึงสามารถทดสอบได้ง่ายอยู่แล้ว - person Rogério; 21.10.2015
comment
การพึ่งพาเช่นนี้สามารถเยาะเย้ยได้ดีอย่างไร? (ถามจริง) - person Andy Turner; 21.10.2015
comment
โปรดทราบว่าคุณสามารถเก็บการกำหนดค่า SimpleJdbcCall ไว้ภายในตัวสร้างได้ เพียงอนุญาตให้การทดสอบสร้างอินสแตนซ์ภายนอกเพื่อให้การทดสอบสามารถแทนที่ executeFunction() ได้ - person Aaron Digulla; 21.10.2015
comment
โดยใช้ไลบรารี การเยาะเย้ย (ตรงข้ามกับไลบรารีอ็อบเจ็กต์จำลองขั้นพื้นฐาน) สำหรับ Java เรามี PowerMockito และ JMockit - person Rogério; 21.10.2015
comment
@ Rogério ดูเหมือนว่าคุณจะมีคำตอบที่ถูกต้อง คุณจะลองเขียนให้ครบถ้วนกว่านี้ไหม - person Andy Turner; 21.10.2015
comment
ฉันเพิ่มตัวอย่างการทดสอบในคำตอบของฉัน - person Rogério; 22.10.2015

เพิ่มตัวสร้างเพิ่มเติม:

/*test*/ MyClass(final SimpleJdbcCall call) {
    simpleJdbcCall = call
        .withoutProcedureColumnMetaDataAccess()
        .withCatalogName("MY_ORACLE_PACKAGE")
        .withFunctionName("GET_VALUE")
        .withReturnValue()
        .declareParameters(
            new SqlOutParameter("RESULT", Types.VARCHAR))
        .declareParameters(
            new SqlParameter("P_VAR1_NAME", Types.VARCHAR))
        .declareParameters(
            new SqlParameter("P_VAR2_NAME", Types.VARCHAR))
        .useInParameterNames("P_VAR1_NAME", "P_VAR2_NAME");
}

อันนี้เป็นแพ็คเกจส่วนตัว ดังนั้นคลาสอื่น ๆ ในแพ็คเกจเดียวกัน (=tests) จึงสามารถเรียกมันได้ ด้วยวิธีนี้ การทดสอบสามารถสร้างอินสแตนซ์ที่มีการแทนที่ executeFunction() ได้ คุณสามารถส่งคืนผลลัพธ์ปลอมในวิธีการหรือทดสอบสถานะของออบเจ็กต์ได้

นั่นหมายความว่าโค้ดของคุณยังคงกำหนดค่าออบเจ็กต์ การทดสอบเพิ่งผ่าน "POJO" ซึ่งมีโค้ดที่อยู่ระหว่างการทดสอบกรอกอยู่

ด้วยวิธีนี้ คุณไม่จำเป็นต้องเขียนโค้ดจำนวนมาก การใช้งานเริ่มต้นจะทำหน้าที่ส่วนใหญ่ให้กับคุณ

หรืออีกทางหนึ่ง อนุญาตให้เรียก Constructor ด้วยอินเทอร์เฟซ SimpleJdbcCallOperations ซึ่งหมายความว่าคุณต้องการเฟรมเวิร์กการเยาะเย้ยที่ทรงพลัง หรือเขียนโค้ด Boiler Plate จำนวนมาก

ทางเลือกอื่น: ใช้ไดรเวอร์ JDBC จำลอง สิ่งเหล่านี้มักจะตั้งค่าได้ยาก ทำให้เกิดความล้มเหลวในการทดสอบปลอม เมื่อการทดสอบล้มเหลว คุณมักจะไม่รู้จริงๆ ว่าทำไม ...

หรือฐานข้อมูลในหน่วยความจำ พวกเขามาพร้อมกับปัญหามากมาย (คุณต้องโหลดข้อมูลทดสอบซึ่งคุณต้องผลิตและบำรุงรักษา)

นั่นเป็นเหตุผลที่ฉันพยายามหลีกเลี่ยงการเดินทางไปกลับผ่านเลเยอร์ JDBC เมื่อทำได้ สมมติว่า JDBC และฐานข้อมูลใช้งานได้ - คนอื่นได้ทดสอบโค้ดนี้แล้ว ถ้าทำอีกจะเสียเวลาเปล่าๆ

ที่เกี่ยวข้อง:

person Aaron Digulla    schedule 21.10.2015

คำแนะนำแรกของฉันคือไม่ทดสอบหน่วย เขียนการทดสอบ บูรณาการ ซึ่งดำเนินการฟังก์ชันที่จัดเก็บไว้ในฐานข้อมูล Oracle จริง (แต่ย้อนกลับธุรกรรม)

มิฉะนั้น คุณสามารถจำลองคลาส SimpleJdbcCall โดยใช้โค้ดที่อยู่ระหว่างการทดสอบได้โดยใช้ PowerMockito หรือ JMockit

ตัวอย่างการทดสอบด้วย JMockit:

@Mocked DataSource ds;
@Mocked SimpleJdbcCall dbCall;

@Test
public void verifyDbCall() {
    String value = new MyClass(ds).getValue("some input");

    // assert on value

    new Verifications() {{
        SqlParameterSource params;
        dbCall.executeFunction(String.class, params = withCapture());
        // JUnit asserts on `params`
    }};
}
person Rogério    schedule 21.10.2015
comment
ความคิดเห็นทั่วไป: การทดสอบการรวมมักจะมีกลิ่นโค้ด (ไม่รู้ว่ากำลังทำอะไรอยู่) - หากพวกเขารู้ พวกเขาสามารถเขียนการทดสอบหน่วยที่เรียบง่าย ขนาดเล็ก และมีประสิทธิภาพได้ เนื่องจากพวกเขาไม่ได้ทำ พวกเขาแค่ทดสอบบางอย่างและหลายๆ อย่าง แต่สุดท้ายแล้ว ก็ไม่มีใครสามารถพูดได้ว่าอะไรทดสอบและอะไรไม่ได้จริงๆ - person Aaron Digulla; 22.10.2015
comment
ฉันต้องยอมรับว่าไม่คุ้นเคยกับ JMockit: dbCall ในตัวอย่างของคุณเกี่ยวข้องกับ MyClass อย่างไร - person Andy Turner; 22.10.2015
comment
@AaronDigulla นั่นไร้สาระ ไม่มีใครสนับสนุนให้ทำการทดสอบหน่วย เท่านั้น ตามที่คุณแนะนำ เนื่องจากพวกเขาไม่ค่อยเก่งในการค้นหาจุดบกพร่องจริงๆ ฉันทามติทั่วไป AFAIK คือให้มีการทดสอบหน่วย ทั้งสอง และการทดสอบบูรณาการบางประเภท แม้ว่าโดยส่วนตัวแล้ว ฉันชอบที่จะมีการทดสอบบูรณาการเท่านั้น เนื่องจากมันทำงานได้ดีมากสำหรับฉันในหลายโครงการ - person Rogério; 22.10.2015
comment
@AndyTurner dbCall อินสแตนซ์ที่กำหนดให้กับฟิลด์จำลองเป็นตัวแทนของอินสแตนซ์ ทั้งหมด SimpleJdbcCall ที่ใช้ในระหว่างการทดสอบ ด้วยเหตุนี้ จึงสามารถใช้เพื่อบันทึกและ/หรือตรวจสอบความคาดหวังที่จะตรงกับการร้องขอในกรณีอื่นๆ - person Rogério; 22.10.2015
comment
@Rogério: ผู้คนต่างสร้างประสบการณ์ที่แตกต่างกัน ฉันเคยเห็นหลายโครงการที่มีการคัดลอกข้อมูลการผลิตไปยังการทดสอบ และฝ่ายบริหารคิดว่าแค่นี้ก็เพียงพอแล้ว ดังนั้นฉันจึงพยายามเตือนผู้คนว่าคุณต้องเข้าใจว่าคุณกำลังทำอะไรอยู่ การทำการทดสอบข้อมูลจำนวนมากเพียงอย่างเดียวนั้นไม่เพียงพอ - person Aaron Digulla; 22.10.2015

ฉันทำโดยใช้ http://www.jmock.org/

การกำหนดค่า XML -

<bean id="simpleJDBCCall" class="org.springframework.jdbc.core.simple.SimpleJdbcCall">
    <property name="jdbcTemplate" ref="jdbcTemplate" />
</bean>

ไฟล์จาวา -

@Autowired
private SimpleJdbcCall jdbcCall;

ชั้นเรียนทดสอบ -

simpleJDBCCall = mockingContext.mock(SimpleJdbcCall.class);
mockingContext.checking(new Expectations() {
        { 
            oneOf(simpleJDBCCall).withSchemaName("test");
            will(returnValue(simpleJDBCCall));
            oneOf(simpleJDBCCall).withCatalogName("test");
            will(returnValue(simpleJDBCCall));
            oneOf(simpleJDBCCall).withProcedureName(ProcedureNames.TEST);
            will(returnValue(simpleJDBCCall));
            oneOf(simpleJDBCCall).execute(5);
            will(returnValue(testMap));
        }
person Java_Alert    schedule 08.11.2016