เอาชนะการผูกมัดใน Guice


138

ฉันเพิ่งเริ่มเล่นกับ Guice และกรณีใช้งานที่ฉันคิดได้ก็คือในการทดสอบฉันแค่ต้องการแทนที่การเชื่อมโยงเดียว ฉันคิดว่าฉันต้องการใช้การเชื่อมโยงระดับการผลิตที่เหลือเพื่อให้แน่ใจว่าทุกอย่างมีการตั้งค่าอย่างถูกต้องและเพื่อหลีกเลี่ยงการทำซ้ำ

ลองจินตนาการว่าฉันมีโมดูลต่อไปนี้

public class ProductionModule implements Module {
    public void configure(Binder binder) {
        binder.bind(InterfaceA.class).to(ConcreteA.class);
        binder.bind(InterfaceB.class).to(ConcreteB.class);
        binder.bind(InterfaceC.class).to(ConcreteC.class);
    }
}

และในการทดสอบของฉันฉันต้องการแทนที่ InterfaceC เท่านั้นในขณะที่รักษา InterfaceA และ InterfaceB ไว้ด้วยดังนั้นฉันจึงต้องการ:

Module testModule = new Module() {
    public void configure(Binder binder) {
        binder.bind(InterfaceC.class).to(MockC.class);
    }
};
Guice.createInjector(new ProductionModule(), testModule);

ฉันได้ลองทำสิ่งต่อไปนี้โดยไม่มีโชค:

Module testModule = new ProductionModule() {
    public void configure(Binder binder) {
        super.configure(binder);
        binder.bind(InterfaceC.class).to(MockC.class);
    }
};
Guice.createInjector(testModule);

ไม่มีใครรู้ว่าเป็นไปได้ที่จะทำสิ่งที่ฉันต้องการหรือฉันเห่าต้นไม้ผิด?

--- ติดตาม: ดูเหมือนว่าฉันสามารถบรรลุสิ่งที่ฉันต้องการหากฉันใช้ประโยชน์จากแท็ก @ImplementedBy บนอินเทอร์เฟซแล้วเพียงแค่ให้การเชื่อมโยงในกรณีทดสอบซึ่งทำงานได้ดีเมื่อมีการแมป 1-1 ระหว่าง อินเตอร์เฟสและการใช้งาน

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


7
เช่นวลี "เห่าต้นไม้ผิด": D
Boris Pavlović

คำตอบ:


149

นี่อาจไม่ใช่คำตอบที่คุณกำลังมองหา แต่ถ้าคุณกำลังเขียนการทดสอบหน่วยคุณอาจไม่ควรใช้หัวฉีดและควรฉีดจำลองหรือวัตถุปลอมด้วยมือ

ในทางกลับกันหากคุณต้องการแทนที่การรวมเดี่ยวคุณสามารถใช้Modules.override(..):

public class ProductionModule implements Module {
    public void configure(Binder binder) {
        binder.bind(InterfaceA.class).to(ConcreteA.class);
        binder.bind(InterfaceB.class).to(ConcreteB.class);
        binder.bind(InterfaceC.class).to(ConcreteC.class);
    }
}
public class TestModule implements Module {
    public void configure(Binder binder) {
        binder.bind(InterfaceC.class).to(MockC.class);
    }
}
Guice.createInjector(Modules.override(new ProductionModule()).with(new TestModule()));

ดูรายละเอียดที่นี่

แต่เป็น javadoc สำหรับการModules.overrides(..)แนะนำคุณควรออกแบบโมดูลของคุณในลักษณะที่คุณไม่จำเป็นต้องแทนที่การผูก ในตัวอย่างที่คุณให้คุณสามารถทำได้โดยการย้ายการเชื่อมโยงInterfaceCไปยังโมดูลที่แยกต่างหาก


9
ขอบคุณอัลเบิร์ตที่ทำให้ฉันเดินไปตามทางที่ฉันต้องการ ที่อยู่ในการผลิตการผลิตยังสรรพสินค้าใหญ่! และนี่คือสำหรับการทดสอบบูรณาการไม่ได้ทดสอบหน่วยซึ่งผมว่าทำไมผมต้องการให้ทุกอย่างอื่นจะถูกสร้างขึ้นอย่างถูกต้อง
tddmonkey

1
ฉันได้เพิ่มตัวอย่างที่เป็นรูปธรรมในรหัส มันช่วยพาคุณไปไกลกว่านี้หรือไม่?
albertb

1
เว้นแต่ฉันจะเข้าใจผิดให้ovverideใช้วิธีที่เหมาะสมStageในขณะที่ทำเช่นนั้น (เช่นใช้การพัฒนาอย่างเป็นระบบ)
pdeschen

4
เรื่องขนาด เมื่อกราฟการพึ่งพาของคุณเติบโตขึ้นการเดินสายด้วยมืออาจเป็นเรื่องที่เจ็บปวด นอกจากนี้เมื่อการเดินสายมีการเปลี่ยนแปลงคุณจำเป็นต้องอัพเดตสถานที่เดินสายด้วยตนเองทั้งหมดของคุณ การแทนที่ช่วยให้คุณจัดการกับสิ่งนั้นโดยอัตโนมัติ
yoosiba

3
@pdeschen นั่นคือข้อผิดพลาดใน Guice 3 ว่าฉันคงสำหรับ Guice 4.
Tavian บาร์นส์

9

ทำไมไม่ใช้มรดก? คุณสามารถลบล้างการโยงที่ระบุในoverrideMeวิธีทิ้งการใช้งานที่ใช้ร่วมกันในconfigureวิธีการ

public class DevModule implements Module {
    public void configure(Binder binder) {
        binder.bind(InterfaceA.class).to(TestDevImplA.class);
        overrideMe(binder);
    }

    protected void overrideMe(Binder binder){
        binder.bind(InterfaceC.class).to(ConcreteC.class);
    }
};

public class TestModule extends DevModule {
    @Override
    public void overrideMe(Binder binder) {
        binder.bind(InterfaceC.class).to(MockC.class);
    }
}

และสร้างหัวฉีดของคุณด้วยวิธีนี้:

Guice.createInjector(new TestModule());

3
@Overrideดูเหมือนจะไม่ทำงาน โดยเฉพาะอย่างยิ่งถ้ามันทำในวิธีการที่@Providesบางสิ่ง
Sasanka Panguluri

4

หากคุณไม่ต้องการที่จะเปลี่ยนโมดูลการผลิตของคุณและถ้าคุณมีโครงสร้างโครงการเหมือน Maven เริ่มต้นเช่น

src/test/java/...
src/main/java/...

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


2

คุณต้องการใช้Juckitoซึ่งคุณสามารถประกาศการกำหนดค่าแบบกำหนดเองของคุณสำหรับแต่ละคลาสการทดสอบ

@RunWith(JukitoRunner.class)
class LogicTest {
    public static class Module extends JukitoModule {

        @Override
        protected void configureTest() {
            bind(InterfaceC.class).to(MockC.class);
        }
    }

    @Inject
    private InterfaceC logic;

    @Test
    public testLogicUsingMock() {
        logic.foo();
    }
}

1

ในการตั้งค่าที่แตกต่างกันเรามีมากกว่าหนึ่งกิจกรรมที่กำหนดไว้ในโมดูลที่แยกต่างหาก กิจกรรมที่กำลังถูกแทรกเข้ามานั้นอยู่ใน Android Library Module พร้อมกับโมดูล RoboGuice ของตัวเองในไฟล์ AndroidManifest.xml

การตั้งค่ามีลักษณะเช่นนี้ ในโมดูลห้องสมุดมีคำจำกัดความเหล่านี้:

AndroidManifest.xml:

<application android:allowBackup="true">
    <activity android:name="com.example.SomeActivity/>
    <meta-data
        android:name="roboguice.modules"
        android:value="com.example.MainModule" />
</application>

จากนั้นเรามีประเภทที่ถูกฉีด:

interface Foo { }

การใช้งานค่าเริ่มต้นของ Foo:

class FooThing implements Foo { }

MainModule กำหนดค่าการใช้งาน FooThing สำหรับ Foo:

public class MainModule extends AbstractModule {
    @Override
    protected void configure() {
        bind(Foo.class).to(FooThing.class);
    }
}

และในที่สุดกิจกรรมที่ใช้งาน Foo:

public class SomeActivity extends RoboActivity {
    @Inject
    private Foo foo;
}

ในการบริโภคการประยุกต์ใช้ Android โมดูลที่เราอยากจะใช้แต่สำหรับวัตถุประสงค์ในการทดสอบฉีดของเราเองSomeActivityFoo

public class SomeOtherActivity extends Activity {
    @Override
    protected void onResume() {
        super.onResume();

        Intent intent = new Intent(this, SomeActivity.class);
        startActivity(intent);
    }
}

อาจมีข้อโต้แย้งที่จะเปิดเผยการจัดการโมดูลกับแอปพลิเคชันไคลเอนต์อย่างไรก็ตามเราจำเป็นต้องซ่อนส่วนประกอบที่ถูกฉีดเข้าไปเป็นส่วนใหญ่เพราะ Library Module เป็น SDK และการเปิดเผยชิ้นส่วนนั้นมีความหมายมากกว่า

(โปรดจำไว้ว่านี่คือการทดสอบดังนั้นเราจึงรู้ว่า internActivity ของ SomeActivity และรู้ว่ามันใช้ (มองเห็นบรรจุภัณฑ์) Foo)

วิธีที่ฉันพบว่าการทำงานนั้นสมเหตุสมผล ใช้การแทนที่ที่แนะนำสำหรับการทดสอบ :

public class SomeOtherActivity extends Activity {
    private class OverrideModule
            extends AbstractModule {

        @Override
        protected void configure() {
            bind(Foo.class).to(OtherFooThing.class);
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        RoboGuice.overrideApplicationInjector(
                getApplication(),
                RoboGuice.newDefaultRoboModule(getApplication()),
                Modules
                        .override(new MainModule())
                        .with(new OverrideModule()));
    }

    @Override
    protected void onResume() {
        super.onResume();

        Intent intent = new Intent(this, SomeActivity.class);
        startActivity(intent);
    }
}

ตอนนี้เมื่อSomeActivityเริ่มต้นมันจะได้รับอินสแตนซ์ที่OtherFooThingถูกฉีดFoo

เป็นสถานการณ์ที่เฉพาะเจาะจงอย่างยิ่งซึ่งในกรณีของเรา OtherFooThing ถูกใช้ภายในเพื่อบันทึกสถานการณ์การทดสอบในขณะที่ FooThing ถูกใช้เป็นค่าเริ่มต้นสำหรับการใช้งานอื่นทั้งหมด

เก็บไว้ในใจที่เราจะใช้#newDefaultRoboModuleในการทดสอบหน่วยของเราและมันทำงานไม่มีที่ติ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.