Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Beyond JUnit: Pragmatic Ways to Increase Code Quality

Beyond JUnit: Pragmatic Ways to Increase Code Quality

You and your team are writing and running unit tests - great! Better yet, they pass (most of the time)! But can you do more to ensure the quality of your code? Come to this talk where you will learn some practical new skills to help increase the quality of your code and catch bugs early in the development cycle. You'll learn how ArchUnit can help you write tests for your application architecture, and how TestCcontainers can help ease your integration testing burden. At the end of this talk, you will know several new techniques to bring to your codebase that will ultimately make your customers happy. This talk is aimed at Java developers who have some basic testing knowledge and want to move to the next level.

Todd Ginsberg

August 15, 2022
Tweet

More Decks by Todd Ginsberg

Other Decks in Technology

Transcript

  1. Beyond JUnit:
    TriJUG
    2022-08-15
    Todd Ginsberg
    Platform Architect
    Pragmatic Ways to
    Increase Code Quality
    @ToddGinsberg

    View Slide

  2. @ToddGinsberg
    Beyond JUnit:
    Pragmatic Ways to
    Increase Code Quality

    View Slide

  3. @ToddGinsberg
    Beyond JUnit:
    Pragmatic Ways to
    Increase Code Quality

    View Slide

  4. @ToddGinsberg
    Beyond JUnit:
    Pragmatic Ways to
    Increase Code Quality
    “Dealing with things sensibly and realistically in a
    way that is based on practical rather than
    theoretical considerations.”
    (Thanks, Google!)

    View Slide

  5. @ToddGinsberg
    I Am Making Some Assumptions About You…

    View Slide

  6. @ToddGinsberg
    I Am Making Some Assumptions About You…
    ● You already write some kind of tests using a modern
    framework like JUnit 5.

    View Slide

  7. @ToddGinsberg
    I Am Making Some Assumptions About You…
    ● You already write some kind of tests using a modern
    framework like JUnit 5.
    ● You have covered the basics of code quality with static
    analysis tools, and maybe an enforced coding style.

    View Slide

  8. @ToddGinsberg
    What You Will See In This Talk

    View Slide

  9. @ToddGinsberg
    What You Will See In This Talk
    1) How to test the architecture of your code.

    View Slide

  10. @ToddGinsberg
    What You Will See In This Talk
    1) How to test the architecture of your code.
    2) How to test that your code plays well with others.

    View Slide

  11. Two Things
    To Keep In
    Mind

    View Slide

  12. @ToddGinsberg
    Any fool can write code that a
    computer can understand. Good
    programmers write code that
    humans can understand.
    – Martin Fowler
    Refactoring: Improving the
    Design of Existing Code

    View Slide

  13. @ToddGinsberg
    Any fool can write code that a
    computer can understand. Good
    programmers write code that
    humans can understand.
    – Martin Fowler
    Refactoring: Improving the
    Design of Existing Code

    View Slide

  14. @ToddGinsberg
    Testing shows the presence,
    not the absence of bugs.
    – Edsger W. Dijkstra
    October, 1969

    View Slide

  15. Architectural
    Testing

    View Slide

  16. @ToddGinsberg
    What’s the Problem?

    View Slide

  17. @ToddGinsberg
    What’s the Problem?
    Photo by Dawid Zawiła on Unsplash

    View Slide

  18. @ToddGinsberg
    What’s the Problem?
    API
    Layer

    View Slide

  19. @ToddGinsberg
    What’s the Problem?
    API
    Layer
    Service
    Layer

    View Slide

  20. @ToddGinsberg
    What’s the Problem?
    API
    Layer
    Service
    Layer
    Data
    Layer

    View Slide

  21. @ToddGinsberg
    What’s the Problem?
    API
    Layer
    Service
    Layer
    Data
    Layer

    View Slide

  22. @ToddGinsberg
    What’s the Problem?
    API
    Layer
    Service
    Layer
    Data
    Layer

    View Slide

  23. @ToddGinsberg
    What’s the Problem?
    API Layer Service Layer Data Layer
    Controller Service
    Controller Service
    Repository
    Repository

    View Slide

  24. @ToddGinsberg
    What’s the Problem?
    API Layer Service Layer Data Layer
    Controller Service
    Controller Service
    Repository
    Repository

    View Slide

  25. @ToddGinsberg
    What’s the Problem?
    API Layer Service Layer Data Layer
    Controller Service
    Controller Service
    Repository
    Repository

    View Slide

  26. @ToddGinsberg
    What’s the Problem?
    API Layer Service Layer Data Layer
    Controller Service
    Controller Service
    Repository
    Repository

    View Slide

  27. @ToddGinsberg
    How Do We Enforce Architectural Rules?

    View Slide

  28. @ToddGinsberg
    How Do We Enforce Architectural Rules?
    BIGCORP_Architecture_Rules_2003(1)_FINAL_UPDATE-3a.doc

    View Slide

  29. @ToddGinsberg
    How Do We Enforce Architectural Rules?

    View Slide

  30. @ToddGinsberg
    How Do We Enforce Architectural Rules?
    10
    YEARS
    AGO!

    View Slide

  31. @ToddGinsberg
    How Do We Enforce Architectural Rules?
    LEFT
    NINE
    YEARS
    AGO!

    View Slide

  32. @ToddGinsberg
    How Do We Enforce Architectural Rules?

    View Slide

  33. @ToddGinsberg
    How Do We Enforce Architectural Rules?

    View Slide

  34. @ToddGinsberg
    What We Want

    View Slide

  35. @ToddGinsberg
    What We Want
    Automatic

    View Slide

  36. @ToddGinsberg
    What We Want
    Automatic
    Accessable

    View Slide

  37. @ToddGinsberg
    What We Want
    Automatic
    Accessable
    Fast

    View Slide

  38. @ToddGinsberg
    ArchUnit To The Rescue!
    https://archunit.org

    View Slide

  39. @ToddGinsberg
    Adding To Our Project
    testImplementation(
    'com.tngtech.archunit:archunit-junit5:0.23.1'
    )

    View Slide

  40. @ToddGinsberg
    Adding To Our Project
    testImplementation(
    'com.tngtech.archunit:archunit-junit5:0.23.1'
    )

    com.tngtech.archunit
    archunit-junit5
    0.23.1
    test

    View Slide

  41. @ToddGinsberg
    Our First Architectural Test…
    SomeServiceImpl

    View Slide

  42. @ToddGinsberg
    Our First Architectural Test…
    SomeServiceImpl

    View Slide

  43. @ToddGinsberg
    Our First Architectural Test…
    @Test
    public void implNamingRule() {
    }

    View Slide

  44. @ToddGinsberg
    Our First Architectural Test…
    @Test
    public void implNamingRule() {
    JavaClasses classes = // TODO
    }

    View Slide

  45. @ToddGinsberg
    Our First Architectural Test…
    @Test
    public void implNamingRule() {
    JavaClasses classes = // TODO
    ArchRule theRule = // TODO
    }

    View Slide

  46. @ToddGinsberg
    Our First Architectural Test…
    @Test
    public void implNamingRule() {
    JavaClasses classes = // TODO
    ArchRule theRule = // TODO
    theRule.check(classes);
    }

    View Slide

  47. @ToddGinsberg
    Our First Architectural Test…
    @Test
    public void implNamingRule() {
    JavaClasses classes = new ClassFileImporter()
    .importPackages(
    "com.example.donutapi"
    );
    }

    View Slide

  48. @ToddGinsberg
    Our First Architectural Test…
    @Test
    public void implNamingRule() {
    JavaClasses classes = new ClassFileImporter()
    .importPackages(
    "com.example.donutapi",
    "com.example.utilities"
    );
    }

    View Slide

  49. @ToddGinsberg
    Our First Architectural Test…
    @Test
    public void implNamingRule() {
    JavaClasses classes = new ClassFileImporter()
    .importPackages("com.example.donutapi");
    ArchRule theRule =
    theRule.check(classes);
    }

    View Slide

  50. @ToddGinsberg
    Our First Architectural Test…
    @Test
    public void implNamingRule() {
    JavaClasses classes = new ClassFileImporter()
    .importPackages("com.example.donutapi");
    ArchRule theRule = noClasses()
    theRule.check(classes);
    }

    View Slide

  51. @ToddGinsberg
    Our First Architectural Test…
    @Test
    public void implNamingRule() {
    JavaClasses classes = new ClassFileImporter()
    .importPackages("com.example.donutapi");
    ArchRule theRule = noClasses()
    .should()
    theRule.check(classes);
    }

    View Slide

  52. @ToddGinsberg
    Our First Architectural Test…
    @Test
    public void implNamingRule() {
    JavaClasses classes = new ClassFileImporter()
    .importPackages("com.example.donutapi");
    ArchRule theRule = noClasses()
    .should()
    .haveNameMatching(".*Impl");
    theRule.check(classes);
    }

    View Slide

  53. @ToddGinsberg
    Our First Architectural Test…
    ArchRule theRule = noClasses()
    .should()
    .haveNameMatching(".*Impl");

    View Slide

  54. @ToddGinsberg
    Our First Architectural Test…
    ArchRule theRule = noClasses()
    .should()
    .haveNameMatching(".*Impl");
    Architecture Violation [Priority: MEDIUM] - Rule 'no classes
    should have name matching '.*Impl'' was violated (1 times):
    Class matches
    '.*Impl' in (SomeServiceImpl.java:0)

    View Slide

  55. @ToddGinsberg
    Our First Architectural Test…
    ArchRule theRule = noClasses()
    .should()
    .haveNameMatching(".*Impl");
    Architecture Violation [Priority: MEDIUM] - Rule 'no classes
    should have name matching '.*Impl'' was violated (1 times):
    Class matches
    '.*Impl' in (SomeServiceImpl.java:0)

    View Slide

  56. @ToddGinsberg
    Our First Architectural Test…
    ArchRule theRule = noClasses()
    .should()
    .haveNameMatching(".*Impl");
    Architecture Violation [Priority: MEDIUM] - Rule 'no classes
    should have name matching '.*Impl'' was violated (1 times):
    Class matches
    '.*Impl' in (SomeServiceImpl.java:0)

    View Slide

  57. @ToddGinsberg
    Our First Architectural Test…
    ArchRule theRule = noClasses()
    .should()
    .haveNameMatching(".*Impl");
    Architecture Violation [Priority: MEDIUM] - Rule 'no classes
    should have name matching '.*Impl'' was violated (1 times):
    Class matches
    '.*Impl' in (SomeServiceImpl.java:0)

    View Slide

  58. @ToddGinsberg
    Our First Architectural Test…
    ArchRule theRule = noClasses()
    .should()
    .haveNameMatching(".*Impl");

    View Slide

  59. @ToddGinsberg
    Our First Architectural Test…
    ArchRule theRule = noClasses()
    .should()
    .haveNameMatching(".*Impl")
    .because("it doesn't tell us anything useful");

    View Slide

  60. @ToddGinsberg
    Our First Architectural Test…
    ArchRule theRule = noClasses()
    .should()
    .haveNameMatching(".*Impl")
    .because("it doesn't tell us anything useful");
    Architecture Violation [Priority: MEDIUM] - Rule 'no classes
    should have name matching '.*Impl', because it doesn't tell us
    anything useful' was violated (1 times):
    Class matches
    '.*Impl' in (SomeServiceImpl.java:0)

    View Slide

  61. @ToddGinsberg
    Our First Architectural Test (Simplified)
    @AnalyzeClasses(
    packages = {"com.example.donutapi"},
    importOptions = {ImportOption.DoNotIncludeTests.class}
    )
    public class ExampleTest {
    @ArchTest
    static final ArchRule implNaming = noClasses()
    .should()
    .haveNameMatching(".*Impl")
    .because("it doesn't tell us anything useful");
    }

    View Slide

  62. @ToddGinsberg
    Our First Architectural Test (Simplified)
    @AnalyzeClasses(
    packages = {"com.example.donutapi"},
    importOptions = {ImportOption.DoNotIncludeTests.class}
    )
    public class ExampleTest {
    @ArchTest
    static final ArchRule implNaming = noClasses()
    .should()
    .haveNameMatching(".*Impl")
    .because("it doesn't tell us anything useful");
    }

    View Slide

  63. @ToddGinsberg
    Our First Architectural Test (Simplified)
    @AnalyzeClasses(
    packages = {"com.example.donutapi"},
    importOptions = {ImportOption.DoNotIncludeTests.class}
    )
    public class ExampleTest {
    @ArchTest
    static final ArchRule implNaming = noClasses()
    .should()
    .haveNameMatching(".*Impl")
    .because("it doesn't tell us anything useful");
    }

    View Slide

  64. @ToddGinsberg
    Our First Architectural Test (Simplified)
    @AnalyzeClasses(
    packages = {"com.example.donutapi"},
    importOptions = {ImportOption.DoNotIncludeTests.class}
    )
    public class ExampleTest {
    @ArchTest
    static final ArchRule implNaming = noClasses()
    .should()
    .haveNameMatching(".*Impl")
    .because("it doesn't tell us anything useful");
    }

    View Slide

  65. @ToddGinsberg
    Our First Architectural Test (Simplified)
    @AnalyzeClasses(
    packages = {"com.example.donutapi"},
    importOptions = {ImportOption.DoNotIncludeTests.class}
    )
    public class ExampleTest {
    @ArchTest
    static final ArchRule implNaming = noClasses()
    .should()
    .haveNameMatching(".*Impl")
    .because("it doesn't tell us anything useful");
    }

    View Slide

  66. @ToddGinsberg
    Our First Architectural Test (Simplified)
    @AnalyzeClasses(
    packages = {"com.example.donutapi"},
    importOptions = {ImportOption.DoNotIncludeTests.class}
    )
    public class ExampleTest {
    @ArchTest
    static final ArchRule implNaming = noClasses()
    .should()
    .haveNameMatching(".*Impl")
    .because("it doesn't tell us anything useful");
    }

    View Slide

  67. @ToddGinsberg
    Our First Architectural Test (Simplified)
    @ArchTest
    static final ArchRule implNaming = noClasses()
    .should()
    .haveNameMatching(".*Impl")
    .because("it doesn't tell us anything useful");

    View Slide

  68. @ToddGinsberg
    Our First Architectural Test (Simplified)
    noClasses()
    .should()
    .haveNameMatching(".*Impl")
    .because("it doesn't tell us anything useful");

    View Slide

  69. @ToddGinsberg
    Use Case: Prohibit Dependencies
    noClasses()
    .should()

    View Slide

  70. @ToddGinsberg
    Use Case: Prohibit Dependencies
    noClasses()
    .should()
    .dependOnClassesThat()

    View Slide

  71. @ToddGinsberg
    Use Case: Prohibit Dependencies
    noClasses()
    .should()
    .dependOnClassesThat()
    .resideInAnyPackage(
    "org.apache.log4j",
    "org.apache.logging.log4j"
    )

    View Slide

  72. @ToddGinsberg
    Use Case: Prohibit Dependencies
    noClasses()
    .should()
    .dependOnClassesThat()
    .resideInAnyPackage(
    "org.apache.log4j",
    "org.apache.logging.log4j"
    )
    .because("our standard is logback");

    View Slide

  73. @ToddGinsberg
    Use Case: Prohibit Dependencies
    Architecture Violation [Priority: MEDIUM] - Rule 'no classes
    should depend on classes that reside in any package
    ['org.apache.log4j', 'org.apache.logging.log4j'], because our
    standard is logback' was violated (2 times):

    View Slide

  74. @ToddGinsberg
    Simplify Violation Messages
    Architecture Violation [Priority: MEDIUM] - Rule 'no classes
    should depend on classes that reside in any package
    ['org.apache.log4j', 'org.apache.logging.log4j'], because our
    standard is logback' was violated (2 times):

    View Slide

  75. @ToddGinsberg
    Simplify Violation Messages
    noClasses()
    .should()
    .dependOnClassesThat()
    .resideInAnyPackage(
    "org.apache.log4j",
    "org.apache.logging.log4j"
    )
    .because("our standard is logback");

    View Slide

  76. @ToddGinsberg
    Simplify Violation Messages
    noClasses()
    .should()
    .dependOnClassesThat()
    .resideInAnyPackage(
    "org.apache.log4j",
    "org.apache.logging.log4j"
    )
    .as("stop using log4j")
    .because("our standard is logback");

    View Slide

  77. @ToddGinsberg
    Simplify Violation Messages
    Architecture Violation [Priority: MEDIUM] - Rule 'stop using
    log4j, because our standard is logback' was violated (2 times):

    View Slide

  78. @ToddGinsberg
    Simplify Violation Messages
    Architecture Violation [Priority: MEDIUM] - Rule 'stop using
    log4j, because our standard is logback' was violated (2 times):

    View Slide

  79. @ToddGinsberg
    Parts of a Rule
    classes()
    .that()
    .areAnnotatedWith(Controller.class)
    .or().areAnnotatedWith(RestController.class)
    .should()
    .resideInAPackage("..controller..")
    .because("controllers belong in a controller package");

    View Slide

  80. @ToddGinsberg
    Parts of a Rule
    classes()
    .that()
    .areAnnotatedWith(Controller.class)
    .or().areAnnotatedWith(RestController.class)
    .should()
    .resideInAPackage("..controller..")
    .because("controllers belong in a controller package");

    View Slide

  81. @ToddGinsberg
    Parts of a Rule
    classes()
    .that()
    .areAnnotatedWith(Controller.class)
    .or().areAnnotatedWith(RestController.class)
    .should()
    .resideInAPackage("..controller..")
    .because("controllers belong in a controller package");

    View Slide

  82. @ToddGinsberg
    Class Predicates
    areAnnotatedWith
    areAnnotations
    areAnonymousClasses
    areAssignableFrom
    areAssignableTo
    areEnums
    areInnerClasses
    areInterfaces
    areLocalClasses
    areMemberClasses
    areMetaAnnotatedWith
    areNestedClasses
    areNotAnnotatedWith
    areNotAnnotations
    areNotAnonymousClasses
    areNotAssignableFrom
    areNotAssignableTo
    areNotEnums
    areNotInnerClasses
    areNotInterfaces
    areNotLocalClasses
    areNotMemberClasses
    areNotMetaAnnotatedWith
    areNotNestedClasses
    areNotPackagePrivate
    areNotPrivate
    areNotProtected
    areNotPublic
    areNotRecords
    areNotTopLevelClasses
    arePackagePrivate
    arePrivate

    View Slide

  83. @ToddGinsberg
    Class Predicates
    areProtected
    arePublic
    areRecords
    areTopLevelClasses
    belongToAnyOf
    containAnyCodeUnitsThat
    containAnyConstructorsThat
    containAnyFieldsThat
    containAnyMembersThat
    containAnyMethodsThat
    containAnyStaticInitializersThat
    doNotBelongToAnyOf
    doNotHaveFullyQualifiedName
    doNotHaveModifier
    doNotHaveSimpleName
    doNotImplement
    haveFullyQualifiedName
    haveModifier
    haveNameMatching
    haveNameNotMatching
    haveSimpleName
    haveSimpleNameContaining
    haveSimpleNameEndingWith
    haveSimpleNameNotContaining
    haveSimpleNameNotEndingWith
    haveSimpleNameNotStartingWith
    haveSimpleNameStartingWith
    implement
    resideInAPackage
    resideInAnyPackage
    resideOutsideOfPackage
    resideOutsideOfPackages

    View Slide

  84. @ToddGinsberg
    Parts of a Rule
    classes()
    .that()
    .areAnnotatedWith(Controller.class)
    .or().areAnnotatedWith(RestController.class)
    .should()
    .resideInAPackage("..controller..")
    .because("controllers belong in a controller package");

    View Slide

  85. @ToddGinsberg
    Class Conditions
    accessClassesThat
    accessField
    accessFieldWhere
    accessTargetWhere
    be
    beAnnotatedWith
    beAnonymousClasses
    beAssignableFrom
    beAssignableTo
    beEnums
    beInnerClasses
    beInterfaces
    beLocalClasses
    beMemberClasses
    beMetaAnnotatedWith
    beNestedClasses
    bePackagePrivate
    bePrivate
    beProtected
    bePublic
    beRecords
    beTopLevelClasses
    callCodeUnitWhere
    callConstructor
    callConstructorWhere
    callMethod
    callMethodWhere
    containNumberOfElements
    dependOnClassesThat
    getField
    getFieldWhere
    haveFullyQualifiedName

    View Slide

  86. @ToddGinsberg
    Class Conditions
    haveModifier
    haveNameMatching
    haveNameNotMatching
    haveOnlyFinalFields
    haveOnlyPrivateConstructors
    haveSimpleName
    haveSimpleNameContaining
    haveSimpleNameEndingWith
    haveSimpleNameNotContaining
    haveSimpleNameNotEndingWith
    haveSimpleNameNotStartingWith
    haveSimpleNameStartingWith
    implement
    notBe
    notBeAnnotatedWith
    notBeAnonymousClasses
    notBeAssignableFrom
    notBeAssignableTo
    notBeEnums
    notBeInnerClasses
    notBeInterfaces
    notBeLocalClasses
    notBeMemberClasses
    notBeMetaAnnotatedWith
    notBeNestedClasses
    notBePackagePrivate
    notBePrivate
    notBeProtected
    notBePublic
    notBeRecords

    View Slide

  87. @ToddGinsberg
    Class Conditions
    notBeRecords
    notBeTopLevelClasses
    notHaveFullyQualifiedName
    notHaveModifier
    notHaveSimpleName
    notImplement
    onlyAccessClassesThat
    onlyAccessFieldsThat
    onlyAccessMembersThat
    onlyCallCodeUnitsThat
    onlyCallConstructorsThat
    onlyCallMethodsThat
    onlyDependOnClassesThat
    onlyHaveDependentClassesThat
    resideInAnyPackage
    resideInAPackage
    resideOutsideOfPackage
    resideOutsideOfPackages
    setField
    setFieldWhere
    transitivelyDependOnClassesThat

    View Slide

  88. @ToddGinsberg
    Positive or Negative Inclusion
    classes()
    .that()
    .areAnnotatedWith(Controller.class)
    .or().areAnnotatedWith(RestController.class)
    .should()
    .resideInAPackage("..controller..")
    .because("controllers belong in a controller package");

    View Slide

  89. @ToddGinsberg
    Positive or Negative Inclusion
    noClasses()
    .that()
    .areAnnotatedWith(Controller.class)
    .or().areAnnotatedWith(RestController.class)
    .should()
    .resideOutsideOfPackage("..controller..")
    .because("controllers belong in a controller package");

    View Slide

  90. @ToddGinsberg
    Methods…
    methods()
    .that()
    .areDeclaredInClassesThat()
    .haveNameMatching(".*Utils?")
    .should()
    .beStatic();

    View Slide

  91. @ToddGinsberg
    Fields…
    fields()
    .that()
    .areStatic()
    .should()
    .beFinal();

    View Slide

  92. @ToddGinsberg
    Use Case: Layered Architecture
    API Layer Service Layer Data Layer
    Controller Service
    Controller Service
    Repository
    Repository

    View Slide

  93. @ToddGinsberg
    Use Case: Layered Architecture
    The hard way…

    View Slide

  94. @ToddGinsberg
    Use Case: Layered Architecture
    The hard way…
    noClasses()
    .that()
    .resideOutsideOfPackage("..controller..")
    .should()
    .accessClassesThat()
    .resideInAnyPackage("..controller..");

    View Slide

  95. @ToddGinsberg
    Use Case: Layered Architecture
    The hard way…
    classes()
    .that()
    .resideInAnyPackage("..service..")
    .should()
    .onlyBeAccessed()
    .byAnyPackage("..controller..");

    View Slide

  96. @ToddGinsberg
    Use Case: Layered Architecture
    The easy way…
    layeredArchitecture()

    View Slide

  97. @ToddGinsberg
    Use Case: Layered Architecture
    The easy way…
    layeredArchitecture()
    .layer("Controller").definedBy("..controller..")
    .layer("Service").definedBy("..service..")
    .layer("Data").definedBy("..data..")

    View Slide

  98. @ToddGinsberg
    Use Case: Layered Architecture
    The easy way…
    layeredArchitecture()
    .layer("Controller").definedBy("..controller..")
    .layer("Service").definedBy("..service..")
    .layer("Data").definedBy("..data..")
    .whereLayer("Controller").mayNotBeAccessedByAnyLayer()
    .whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
    .whereLayer("Data").mayOnlyBeAccessedByLayers("Service");

    View Slide

  99. @ToddGinsberg
    Use Case: Onion Architecture
    The easy way…
    onionArchitecture()

    View Slide

  100. @ToddGinsberg
    Use Case: Onion Architecture
    The easy way…
    onionArchitecture()
    .domainModels("..model..")
    .domainServices("..service..")
    .applicationServices("..app..")
    .adapter("cli", "..cli..")
    .adapter("data", "..data..")
    .adapter("rest", "..rest..");

    View Slide

  101. @ToddGinsberg
    Use Case: PlantUML Diagram

    View Slide

  102. @ToddGinsberg
    Use Case: PlantUML Diagram
    @startuml
    [Address] <<..address..>>
    [Customer] <<..customer..>>
    [Order] <<..order..>> #white/PowderBlue
    [Products] <<..product..>> #white/PowderBlue
    [Product Catalog] <<..catalog..>> as catalog #PowderBlue
    [Product Import] <<..importer..>> as import
    [XML] <<..xml.processor..>> <<..xml.types..>> as xml
    [Order] ---> [Customer] : is placed by
    [Order] --> [Products]
    [Customer] --> [Address]
    [Products] <--[#green]- catalog
    import -left-> catalog : parse products
    import --> xml
    @enduml

    View Slide

  103. @ToddGinsberg
    Use Case: PlantUML Diagrams

    View Slide

  104. @ToddGinsberg
    Use Case: PlantUML Diagrams
    URL myDiagram = getClass().getResource("project.puml");

    View Slide

  105. @ToddGinsberg
    Use Case: PlantUML Diagrams
    URL myDiagram = getClass().getResource("project.puml");
    classes()
    .should(
    adhereToPlantUmlDiagram(
    myDiagram,
    consideringAllDependencies()
    )
    );

    View Slide

  106. @ToddGinsberg
    ArchUnit-Provided Rules
    ● Don’t use standard streams (System.out, System.in, printStackTrace)
    ● Don’t throw generic exceptions (Throwable, Exception, RuntimeException)
    ● Don’t use java.util.logging
    ● Don’t use Joda Time
    ● Prevent field-level autowiring (@Autowired, @Value, @Inject, @Resource, etc)

    View Slide

  107. @ToddGinsberg
    ArchUnit-Provided Rules
    noClasses()
    .should(ACCESS_STANDARD_STREAMS);

    View Slide

  108. @ToddGinsberg
    ArchUnit Works Great For…
    Photo by Dawid Zawiła on Unsplash

    View Slide

  109. @ToddGinsberg
    What About Legacy Applications?
    This is a README. Pay attention to it!
    This repository is not a place of honor... no highly
    esteemed code is kept here.
    What is here was dangerous and repulsive to us.
    This repository is best shunned and left
    untouched.

    View Slide

  110. @ToddGinsberg
    Use Case: Freezing Rules
    layeredArchitecture()
    .layer("Controller").definedBy("..controller..")
    .layer("Service").definedBy("..service..")
    .layer("Data").definedBy("..data..")
    .whereLayer("Controller").mayNotBeAccessedByAnyLayer()
    .whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
    .whereLayer("Data").mayOnlyBeAccessedByLayers("Service");

    View Slide

  111. @ToddGinsberg
    Use Case: Freezing Rules
    FreezingArchRule.freeze(
    layeredArchitecture()
    .layer("Controller").definedBy("..controller..")
    .layer("Service").definedBy("..service..")
    .layer("Data").definedBy("..data..")
    .whereLayer("Controller").mayNotBeAccessedByAnyLayer()
    .whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
    .whereLayer("Data").mayOnlyBeAccessedByLayers("Service")
    );

    View Slide

  112. @ToddGinsberg
    Use Case: Freezing Rules
    archunit.properties

    View Slide

  113. @ToddGinsberg
    Use Case: Freezing Rules
    archunit.properties
    Run Tests

    View Slide

  114. @ToddGinsberg
    Use Case: Freezing Rules
    archunit.properties
    Run Tests stored.rules
    uuid-named files
    stored.rules
    uuid-named files

    View Slide

  115. @ToddGinsberg
    Use Case: Freezing Rules
    My Code
    +1

    View Slide

  116. @ToddGinsberg
    Use Case: Freezing Rules
    Run Tests
    My Code
    +1

    View Slide

  117. @ToddGinsberg
    Use Case: Freezing Rules
    My Code
    -1

    View Slide

  118. @ToddGinsberg
    Use Case: Freezing Rules
    Run Tests stored.rules
    uuid-named files
    My Code
    -1

    View Slide

  119. @ToddGinsberg
    Testing Our Rules
    classes()
    .that()
    .resideInAnyPackage("that doesn’t exist")
    .should()
    // ...

    View Slide

  120. @ToddGinsberg
    Testing Our Rules
    classes()
    .that()
    .resideInAnyPackage("that doesn’t exist")
    .should()
    // ...
    This rule will FAIL if should() operates on an
    empty set of classes!

    View Slide

  121. @ToddGinsberg
    Testing Our Rules
    classes()
    .that()
    .resideInAnyPackage("that doesn’t exist")
    .should()
    // Some fun conditions here
    .allowEmptyShould(true)

    View Slide

  122. @ToddGinsberg
    Testing Our Rules
    ArchRule auroraBorealisLocationAndTimeOfDay = //...

    View Slide

  123. @ToddGinsberg
    Testing Our Rules
    ArchRule auroraBorealisLocationAndTimeOfDay = //...
    JavaClasses classes =
    new ClassFileImporter().importClasses(SkinnersKitchen.class);

    View Slide

  124. @ToddGinsberg
    Testing Our Rules
    ArchRule auroraBorealisLocationAndTimeOfDay = //...
    JavaClasses classes =
    new ClassFileImporter().importClasses(SkinnersKitchen.class);
    auroraBorealisLocationAndTimeOfDay.check(classes)

    View Slide

  125. @ToddGinsberg
    Testing Our Rules
    ArchRule auroraBorealisLocationAndTimeOfDay = //...
    JavaClasses classes =
    new ClassFileImporter().importClasses(SkinnersKitchen.class);
    assertThatThrownBy(
    () -> auroraBorealisLocationAndTimeOfDay.check(classes)
    )
    .isInstanceOf(AssertionError.class)
    .hasMessageStartingWith("Architecture Violation");

    View Slide

  126. @ToddGinsberg
    Why Use ArchUnit?
    ● Quickly enforce architectural rules from within our project.

    View Slide

  127. @ToddGinsberg
    Why Use ArchUnit?
    ● Quickly enforce architectural rules from within our project.
    ● Runs as part of test cycle for early, inescapable feedback.

    View Slide

  128. @ToddGinsberg
    Why Use ArchUnit?
    ● Quickly enforce architectural rules from within our project.
    ● Runs as part of test cycle for early, inescapable feedback.
    ● Documents intent.

    View Slide

  129. @ToddGinsberg
    Why Use ArchUnit?
    ● Quickly enforce architectural rules from within our project.
    ● Runs as part of test cycle for early, inescapable feedback.
    ● Documents intent.
    ● Helps move legacy systems towards enforced standards.

    View Slide

  130. Painless
    Integration
    Tests

    View Slide

  131. Unit Tests
    Integration
    Tests
    E2E
    Tests
    Faster
    Slower
    Isolated
    Coupled

    View Slide

  132. Unit Tests

    View Slide

  133. Unit Tests
    Manual
    Tests
    (ewww)

    View Slide

  134. Unit Tests
    Manual
    Tests
    (ewww)
    Integration Tests
    (broken)

    View Slide

  135. @ToddGinsberg
    Why Is Integration Testing So Difficult?

    View Slide

  136. @ToddGinsberg
    Why Is Integration Testing So Difficult?
    Our
    Application

    View Slide

  137. @ToddGinsberg
    Our Machine
    What About An Embedded DB?
    Our
    Application

    View Slide

  138. @ToddGinsberg
    Our Machine
    What About An Embedded DB?
    H2
    Our
    Application

    View Slide

  139. @ToddGinsberg
    Our Machine
    Run Our Own Database?
    Our
    Application

    View Slide

  140. @ToddGinsberg
    Central DB
    Our Machine
    What About A Shared Instance?
    Our
    Application

    View Slide

  141. @ToddGinsberg
    Central DB
    Our Machine
    What About A Shared Instance?
    Our
    Application
    Randos

    View Slide

  142. @ToddGinsberg
    Central DB
    Our Machine
    What About A Shared Instance?
    Our
    Application
    Randos

    View Slide

  143. @ToddGinsberg
    Central DB
    Our Machine
    What About A Shared Instance?
    Our
    Application
    Randos
    V1
    V1

    View Slide

  144. @ToddGinsberg
    Central DB
    Our Machine
    What About A Shared Instance?
    Our
    Application
    Randos
    V1
    V2

    View Slide

  145. @ToddGinsberg
    Our Machine
    What About A Container?
    Our
    Application

    View Slide

  146. @ToddGinsberg
    Our Machine
    What About A Container?
    Our
    Application

    View Slide

  147. @ToddGinsberg
    Testcontainers To The Rescue!
    https://testcontainers.org

    View Slide

  148. @ToddGinsberg
    How Does It Work?

    View Slide

  149. @ToddGinsberg
    Docker Daemon
    How Does It Work?

    View Slide

  150. @ToddGinsberg
    Docker Daemon
    How Does It Work?
    Our Dependencies

    View Slide

  151. @ToddGinsberg
    Docker Daemon
    How Does It Work?
    Docker REST API
    Our Dependencies

    View Slide

  152. @ToddGinsberg
    Docker Daemon
    How Does It Work?
    Docker REST API
    Docker
    CLI
    Our Dependencies

    View Slide

  153. @ToddGinsberg
    Docker Daemon
    How Does It Work?
    Docker REST API
    Our
    Application
    Our Dependencies

    View Slide

  154. @ToddGinsberg
    Docker Daemon
    How Does It Work?
    Docker REST API
    Our
    Application
    Our Dependencies
    Ryuk

    View Slide

  155. @ToddGinsberg
    Testcontainer Requirements
    1) Docker Environment

    View Slide

  156. @ToddGinsberg
    Testcontainer Requirements
    1) Docker Environment
    2) Java 8+

    View Slide

  157. @ToddGinsberg
    Testcontainer Requirements
    1) Docker Environment
    2) Java 8+
    3) JUnit4 or JUnit5 or Spock

    View Slide

  158. @ToddGinsberg
    Testcontainer Requirements
    1) Docker Environment
    2) Java 8+
    3) JUnit4 or JUnit5 or Spock

    View Slide

  159. @ToddGinsberg
    Docker Environment

    View Slide

  160. @ToddGinsberg
    Docker Environment

    View Slide

  161. @ToddGinsberg
    Docker Environment

    View Slide

  162. @ToddGinsberg
    Docker Environment

    View Slide

  163. @ToddGinsberg
    Docker Environment

    View Slide

  164. @ToddGinsberg
    Adding To Our Project
    testImplementation(
    'org.testcontainers:testcontainers:1.17.3',
    'org.testcontainers:junit-jupiter:1.17.3'
    )

    View Slide

  165. @ToddGinsberg
    Adding To Our Project

    org.testcontainers
    testcontainers
    1.17.3
    test


    org.testcontainers
    junit-jupiter
    1.17.3
    test

    View Slide

  166. @ToddGinsberg
    Use Case: Spring Talking to Redis
    Spring
    Application

    View Slide

  167. Redis
    Integration
    Test Demo

    View Slide

  168. @ToddGinsberg
    Use Case: Database Tests
    @SpringBootTest
    public class PostgresRelatedTest {
    @Autowired
    private DonutRepository repository;
    // ???
    }

    View Slide

  169. @ToddGinsberg
    Use Case: Database Tests
    @SpringBootTest
    @Testcontainers
    public class PostgresRelatedTest {
    @Autowired
    private DonutRepository repository;
    // ???
    }

    View Slide

  170. @ToddGinsberg
    Use Case: Database Tests
    @Autowired
    private DonutRepository repository;

    View Slide

  171. @ToddGinsberg
    Use Case: Database Tests
    @Autowired
    private DonutRepository repository;
    @Container
    private static PostgreSQLContainer database =
    new PostgreSQLContainer("postgres:14.2");

    View Slide

  172. @ToddGinsberg
    Use Case: Database Tests
    @Autowired
    private DonutRepository repository;
    @Container
    private static PostgreSQLContainer database =
    new PostgreSQLContainer("postgres:14.2");
    @DynamicPropertySource
    public static void props(DynamicPropertyRegistry registry) {
    registry.add("spring.datasource.url", database::getJdbcUrl);
    registry.add("spring.datasource.username", database::getUsername);
    registry.add("spring.datasource.password", database::getPassword);
    }

    View Slide

  173. @ToddGinsberg
    Use Case: Database Tests
    @Autowired
    private DonutRepository repository;
    @Container
    private static PostgreSQLContainer database =
    new PostgreSQLContainer("postgres:14.2")
    .withDatabaseName("setec_astronomy")
    .withUsername("mbishop")
    .withPassword("T00ManyS3crets");

    View Slide

  174. @ToddGinsberg
    Use Case: Database Tests
    @Autowired
    private DonutRepository repository;
    @Container
    private static PostgreSQLContainer database =
    new PostgreSQLContainer("postgres:14.2")
    .withConnectionTimeoutSeconds(20)
    .withStartupTimeoutSeconds(20);

    View Slide

  175. @ToddGinsberg
    Use Case: Database Tests
    @Autowired
    private DonutRepository repository;
    @Container
    private static PostgreSQLContainer database =
    new PostgreSQLContainer("postgres:14.2")
    .withInitScript("src/init.sql");

    View Slide

  176. @ToddGinsberg
    Supported Database Modules
    JDBC
    R2DBC
    Cassandra
    CockroachDB
    Clickhouse
    DB2
    Dynalite
    Influx
    MariaDB
    MongoDB
    MS SQL Server
    MySQL
    Neo4j
    Oracle-XE
    OrientDB
    Postgres
    Presto
    Trinio

    View Slide

  177. @ToddGinsberg
    Supported Database Modules
    JDBC
    R2DBC
    Cassandra
    CockroachDB
    Clickhouse
    DB2
    Dynalite
    Influx
    MariaDB
    MongoDB
    MS SQL Server
    MySQL
    Neo4j
    Oracle-XE
    OrientDB
    Postgres
    Presto
    Trinio

    View Slide

  178. @ToddGinsberg
    Use Case: Database Tests (But Simpler!)
    jdbc:postgresql://localhost:5432/somedb

    View Slide

  179. @ToddGinsberg
    Use Case: Database Tests (But Simpler!)
    jdbc:tc:postgresql:14.2://localhost:5432/somedb

    View Slide

  180. @ToddGinsberg
    Use Case: Database Tests (But Simpler!)
    jdbc:tc:postgresql:14.2://localhost:5432/somedb
    Our Application
    Testcontainers
    JDBC Driver
    Data Layer

    View Slide

  181. @ToddGinsberg
    Use Case: Database Tests (But Simpler!)
    jdbc:tc:postgresql:14.2://localhost:5432/somedb
    Our Application
    Testcontainers
    JDBC Driver
    Data Layer

    View Slide

  182. @ToddGinsberg
    Use Case: Database Tests (But Simpler!)
    jdbc:tc:postgresql:14.2://localhost:5432/somedb
    Our Application
    Testcontainers
    JDBC Driver
    Data Layer

    View Slide

  183. @ToddGinsberg
    Use Case: Database Tests (But Simpler!)
    jdbc:tc:postgresql:14.2://localhost:5432/somedb
    Our Application
    Testcontainers
    JDBC Driver
    PosgreSQL
    JDBC Driver
    Data Layer

    View Slide

  184. @ToddGinsberg
    Use Case: Database Tests (But Simpler!)
    jdbc:tc:postgresql:14.2://localhost:5432/somedb
    Our Application
    Testcontainers
    JDBC Driver
    PosgreSQL
    JDBC Driver
    Data Layer

    View Slide

  185. @ToddGinsberg
    Use Case: Database Tests (But Simpler!)
    @SpringBootTest
    public class JdbcUrlTest {
    static {
    }
    }

    View Slide

  186. @ToddGinsberg
    Use Case: Database Tests (But Simpler!)
    @SpringBootTest
    public class JdbcUrlTest {
    static {
    System.setProperty(
    "spring.datasource.url",
    "jdbc:tc:postgresql:14.2://localhost:5432/somedb"
    );
    }
    }

    View Slide

  187. @ToddGinsberg
    Use Case: Database Tests (But Simpler!)
    @SpringBootTest
    public class JdbcUrlTest {
    static {
    System.setProperty(
    "spring.datasource.url",
    "jdbc:tc:postgresql:14.2://localhost:5432/somedb"
    );
    }
    }

    View Slide

  188. @ToddGinsberg
    Use Case: Database Tests (But Simpler!)
    @SpringBootTest
    public class JdbcUrlTest {
    static {
    System.setProperty(
    "spring.datasource.url",
    "jdbc:tc:postgresql:14.2://localhost:5432/"
    );
    }
    }

    View Slide

  189. @ToddGinsberg
    Use Case: Database Tests (But Simpler!)
    @SpringBootTest
    public class JdbcUrlTest {
    static {
    System.setProperty(
    "spring.datasource.url",
    "jdbc:tc:postgresql:14.2://localhost:5432/"
    );
    }
    }

    View Slide

  190. @ToddGinsberg
    Use Case: Database Tests (But Simpler!)
    @SpringBootTest
    public class JdbcUrlTest {
    static {
    System.setProperty(
    "spring.datasource.url",
    "jdbc:tc:postgresql:14.2:///"
    );
    }
    }

    View Slide

  191. @ToddGinsberg
    JDBC Init Script Options…
    jdbc:tc:postgresql:14.2:///

    View Slide

  192. @ToddGinsberg
    JDBC Init Script Options…
    jdbc:tc:postgresql:14.2:///
    ?TC_INITSCRIPT=path/init.sql

    View Slide

  193. @ToddGinsberg
    JDBC Init Script Options…
    jdbc:tc:postgresql:14.2:///
    ?TC_INITSCRIPT=file:src/init.sql

    View Slide

  194. @ToddGinsberg
    JDBC Init Script Options…
    jdbc:tc:postgresql:14.2:///
    ?TC_INITFUNCTION=JdbcTest::initFunction

    View Slide

  195. @ToddGinsberg
    JDBC Init Script Options…
    jdbc:tc:postgresql:14.2:///
    ?TC_INITFUNCTION=JdbcTest::initFunction
    public class JdbcTest {
    public static void initFunction(Connection connection)
    throws SQLException {
    // Flyway, Liquibase, etc...
    }
    }

    View Slide

  196. @ToddGinsberg
    Use Case: Database Tests (But Simpler!)
    @SpringBootTest
    public class JdbcUrlTest {
    static {
    System.setProperty(
    "spring.datasource.driverClassName",
    "org.testcontainers.jdbc.ContainerDatabaseDriver"
    );
    System.setProperty(
    "spring.datasource.url",
    "jdbc:tc:postgresql:14.2:///"
    );
    }
    }

    View Slide

  197. @ToddGinsberg
    Use Case: Database Tests (But Simpler!)
    @SpringBootTest
    public class JdbcUrlTest {
    static {
    System.setProperty(
    "spring.datasource.driverClassName",
    "org.testcontainers.jdbc.ContainerDatabaseDriver"
    );
    System.setProperty(
    "spring.datasource.url",
    "jdbc:tc:postgresql:14.2:///"
    );
    }
    }

    View Slide

  198. @ToddGinsberg
    More Than One Container
    OurFirstTestClass

    View Slide

  199. @ToddGinsberg
    More Than One Container
    OurFirstTestClass OurSecondTestClass

    View Slide

  200. @ToddGinsberg
    More Than One Container
    OurFirstTestClass OurSecondTestClass
    AbstractBaseTest

    View Slide

  201. @ToddGinsberg
    In A Base Class For All Tests
    public abstract class AbstractBaseTest {
    static PostgreSQLContainer database;
    static GenericContainer redis;
    static {
    database = new PostgreSQLContainer("postgres:14.2");
    database.start();
    redis = new GenericContainer("redis:6.4.2-alpine");
    redis.start();
    }
    }

    View Slide

  202. MockServer
    Demo

    View Slide

  203. @ToddGinsberg
    More Supported Modules
    Azure
    Docker Compose
    ElasticSearch
    GCloud
    HiveMQ
    K3S
    Kafka
    LocalStack
    MockServer
    Nginx
    Apache Pulsar
    RabbitMQ
    Solr
    Toxiproxy
    Hashicorp Vault
    WebDriver

    View Slide

  204. @ToddGinsberg
    More Supported Modules
    Azure
    Docker Compose
    ElasticSearch
    GCloud
    HiveMQ
    K3S
    Kafka
    LocalStack
    MockServer
    Nginx
    Apache Pulsar
    RabbitMQ
    Solr
    Toxiproxy
    Hashicorp Vault
    WebDriver

    View Slide

  205. @ToddGinsberg
    More Supported Modules
    Azure
    Docker Compose
    ElasticSearch
    GCloud
    HiveMQ
    K3S
    Kafka
    LocalStack
    MockServer
    Nginx
    Apache Pulsar
    RabbitMQ
    Solr
    Toxiproxy
    Hashicorp Vault
    WebDriver

    View Slide

  206. @ToddGinsberg
    More Supported Modules
    Azure
    Docker Compose
    ElasticSearch
    GCloud
    HiveMQ
    K3S
    Kafka
    LocalStack
    MockServer
    Nginx
    Apache Pulsar
    RabbitMQ
    Solr
    Toxiproxy
    Hashicorp Vault
    WebDriver

    View Slide

  207. @ToddGinsberg
    Why Use Testcontainers?
    ● Isolated environments against real components

    View Slide

  208. @ToddGinsberg
    Why Use Testcontainers?
    ● Isolated environments against real components
    ● App start + DB created in a single test

    View Slide

  209. @ToddGinsberg
    Why Use Testcontainers?
    ● Isolated environments against real components
    ● App start + DB created in a single test
    ● Easier to test failure cases

    View Slide

  210. @ToddGinsberg
    Why Use Testcontainers?
    ● Isolated environments against real components
    ● App start + DB created in a single test
    ● Easier to test failure cases
    ● Run tests offline*

    View Slide

  211. @ToddGinsberg
    Why Use Testcontainers?
    ● Isolated environments against real components
    ● App start + DB created in a single test
    ● Easier to test failure cases
    ● Run tests offline*
    ● Code and Integration Tests in the same PR and repo

    View Slide

  212. @ToddGinsberg
    Smuggle Fun Technologies In With Tests!

    View Slide

  213. In
    Conclusion…

    View Slide

  214. https://archunit.org

    View Slide

  215. https://testcontainers.org

    View Slide

  216. @ToddGinsberg
    [email protected]
    https://todd.ginsberg.com
    Thank You!

    View Slide