├── .circleci └── config.yml ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── LICENSE ├── docs ├── _config.yml ├── _layouts │ └── default.html ├── assets │ ├── css │ │ └── style.scss │ └── images │ │ └── favicon.ico ├── benchmark.md ├── expectedException.md ├── index.md ├── randomBeans.md ├── systemProperty.md ├── temporaryFolder.md ├── testName.md └── watcher.md ├── pom.xml ├── readme.md └── src ├── main └── java │ └── io │ └── github │ └── glytching │ └── junit │ └── extension │ ├── benchmark │ ├── BenchmarkExtension.java │ └── StopWatch.java │ ├── exception │ ├── ExpectedException.java │ └── ExpectedExceptionExtension.java │ ├── folder │ ├── TemporaryFolder.java │ ├── TemporaryFolderException.java │ └── TemporaryFolderExtension.java │ ├── random │ ├── Random.java │ └── RandomBeansExtension.java │ ├── system │ ├── RestoreContext.java │ ├── SystemProperties.java │ ├── SystemProperty.java │ └── SystemPropertyExtension.java │ ├── testname │ ├── TestName.java │ └── TestNameExtension.java │ ├── util │ └── ExtensionUtil.java │ └── watcher │ └── WatcherExtension.java └── test └── java └── io └── github └── glytching └── junit └── extension ├── CompositeExtensionTest.java ├── benchmark └── BenchmarkExtensionTest.java ├── exception ├── ExpectedExceptionExtensionMetaTest.java └── ExpectedExceptionExtensionTest.java ├── folder ├── TemporaryFolderExtensionBeforeAllTest.java ├── TemporaryFolderExtensionBeforeEachTest.java ├── TemporaryFolderExtensionParameterTest.java └── TemporaryFolderTest.java ├── random ├── DomainObject.java ├── NestedDomainObject.java ├── RandomBeansExtensionFieldTest.java ├── RandomBeansExtensionOverrideTest.java └── RandomBeansExtensionParameterTest.java ├── system ├── SystemPropertyExtensionClassTest.java ├── SystemPropertyExtensionMetaTest.java └── SystemPropertyExtensionMethodTest.java ├── testname └── TestNameExtensionTest.java ├── util ├── AssertionUtil.java ├── ExecutionEvent.java ├── ExtensionTester.java └── RecordingExecutionListener.java └── watcher ├── WatcherExtensionMetaTest.java └── WatcherExtensionTest.java /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/openjdk:8-jdk 6 | 7 | working_directory: ~/repo 8 | 9 | environment: 10 | TERM: dumb 11 | 12 | steps: 13 | - checkout 14 | 15 | - run: mvn verify jacoco:report coveralls:report -DrepoToken=w7KlgSEVtzzJRRCxI84aCx3kwKOy0DR0r -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | Code Of Conduct 2 | ====== 3 | 4 | A few simple rules in the event that this becomes more than a single person project ... 5 | 6 | - Be friendly and welcoming 7 | - Be patient 8 | - Remember that people have varying communication styles and that not everyone is using their native language. (Meaning and tone can be lost in translation.) 9 | - Be thoughtful 10 | - Productive communication requires effort. Think about how your words will be interpreted. 11 | - Remember that sometimes it is best to refrain entirely from commenting. 12 | - Be respectful 13 | - In particular, respect differences of opinion. 14 | - Be charitable 15 | - Interpret the arguments of others in good faith, do not seek to disagree. 16 | - When we do disagree, try to understand why. 17 | - Avoid destructive behavior: 18 | - Derailing: stay on topic; if you want to talk about something else, start a new conversation. 19 | - Unconstructive criticism: don't merely decry the current state of affairs; offer—or at least solicit—suggestions as to how things may be improved. 20 | - Snarking (pithy, unproductive, sniping comments) 21 | - Discussing potentially offensive or sensitive issues; this all too often leads to unnecessary conflict. 22 | - Microaggressions: brief and commonplace verbal, behavioral and environmental indignities that communicate hostile, derogatory or negative slights and insults to a person or group. -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ====== 3 | 4 | > Contributions are welcome and contribution guidelines will be added here shortly after the first expression of interest from a potential contributor! -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Expected Behavior 2 | 3 | > Describe what you were trying to do 4 | 5 | ## Actual Behavior 6 | 7 | > Describe what actually happened 8 | 9 | ## Steps to Reproduce the Problem 10 | 11 | > Please bear in mind that the more detail you provide the more likely it is that the issue will be resolved. 12 | 13 | 1. 14 | 2. 15 | 3. 16 | 4. 17 | 18 | ## Specifications 19 | 20 | - JUnit-Extensions Version: 21 | - Java Version: 22 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Proposed Change 2 | 3 | Describe the proposed change. Consider the change from the maintainers' perspective; why should this PR be accepted? 4 | 5 | If the PR fixes a bug or resolves a feature request, please include a link to that issue. 6 | 7 | ## Type Of Change 8 | 9 | What type of change does this PR introduce? 10 | 11 | _Put an `x` in the boxes that apply_ 12 | 13 | - [ ] Bugfix (non-breaking change which fixes an issue) 14 | - [ ] New feature (non-breaking change which adds functionality) 15 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 16 | 17 | ## Checklist 18 | 19 | _Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code._ 20 | 21 | - [ ] I have read the [CONTRIBUTING](/CONTRIBUTING.md) doc 22 | - [ ] All unit tests pass locally with my changes 23 | - [ ] I have added tests that prove my fix is effective or that my feature works 24 | - [ ] I have added necessary documentation (if appropriate) 25 | 26 | ## Further Comments 27 | 28 | If this is a non trivial change then initiate the discussion by explaining why you chose the solution you did and what alternatives you considered etc... -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | target/ 3 | *.iml 4 | todo.md -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal 2 | 3 | title: JUnit-Extensions -------------------------------------------------------------------------------- /docs/_layouts/default.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | JUnit-Extensions Documentation 8 | 9 | 10 | 11 | 12 | {% seo %} 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 |
24 |
25 |

{{ site.title | default: site.github.repository_name }}

26 | 27 | 28 | 29 |

Introduction

30 |
31 |

Benchmark

32 |

ExpectedException

33 |

RandomBeans

34 |

SystemProperty

35 |

TemporaryFolder

36 |

TestName

37 |

Watcher

38 | 39 | 40 | 41 |
42 |
43 | 44 | {{ content }} 45 | 46 |
47 | 55 |
56 | 57 | 58 | 59 | {% if site.google_analytics %} 60 | 75 | {% endif %} 76 | 77 | -------------------------------------------------------------------------------- /docs/assets/css/style.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | @import "{{ site.theme }}"; 5 | 6 | /* Modifies the theme style from https://github.com/pages-themes/minimal/blob/master/_sass/jekyll-theme-minimal.scss */ 7 | 8 | header { 9 | width:150px; 10 | } 11 | 12 | section { 13 | width:750px; 14 | } 15 | 16 | footer { 17 | width:150px; 18 | } 19 | 20 | .wrapper { 21 | width:960px; 22 | } 23 | 24 | .body { 25 | padding: 25px 26 | } 27 | 28 | p, ul, ol, table, pre, dl { 29 | margin:0 0 10px; 30 | } 31 | 32 | img { 33 | max-width: 95%; 34 | padding: 5px 0 20px 0; 35 | } -------------------------------------------------------------------------------- /docs/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glytching/junit-extensions/5c498fd7a7a628e4d5f1c272c6bda24c8669d470/docs/assets/images/favicon.ico -------------------------------------------------------------------------------- /docs/benchmark.md: -------------------------------------------------------------------------------- 1 | BenchmarkExtension 2 | ====== 3 | 4 | This extension watches the test invocation without modifying it or interfering with it in any way. Instead, it just reports test elapsed time. The elapsed time is logged in milliseconds, by default, but you can also choose another unit from `java.util.concurrent.TimeUnit`. The elapsed time and test name are published to the test execution context so, by default, they will be written to console. Alternatively, you could provide your own implementation of `EngineExecutionListener` to report the output differently or to aggregate the output or to publish to a different target etc. 5 | 6 | #### Usage 7 | 8 | This extension is engaged by adding the `@ExtendWith(BenchmarkExtension.class)` annotation to a test class or - if you want to choose a non default `TimeUnit` - by using JUnit5's `@RegisterExtension`. 9 | 10 | #### Example 11 | 12 | ``` 13 | @ExtendWith(BenchmarkExtension.class) 14 | public class MyTest { 15 | 16 | // elapsed time will be logged for every test in this test case 17 | 18 | } 19 | ``` 20 | 21 | ``` 22 | public class MyTest { 23 | 24 | @RegisterExtension 25 | static BenchmarkExtension benchmarkExtension = new BenchmarkExtension(TimeUnit.MICROSECONDS); 26 | 27 | // elapsed time will be logged, in MICROSECONDS, for every test in this test case 28 | 29 | } 30 | ``` 31 | 32 | #### Output 33 | 34 | Example output 35 | 36 | ``` 37 | timestamp = 2018-08-30T16:47:07.352, Elapsed time in MILLISECONDS for canBenchmark = 6 38 | ``` -------------------------------------------------------------------------------- /docs/expectedException.md: -------------------------------------------------------------------------------- 1 | ExpectedExceptionExtension 2 | ====== 3 | 4 | Historically, assertions against an exception thrown by a JUnit test case involved one of the following: 5 | 6 | 1. Wrap the test case code in a `try/fail/catch` idiom 7 | 1. Use the `expected` element of the `@Test` annotation 8 | 1. Use JUnit4's `ExpectedException` rule 9 | 10 | In JUnit5: 11 | 12 | 1. The wrap-and-assert approach now has some lambda sugar, for example: 13 | 14 | ``` 15 | RuntimeException actual = assertThrows(RuntimeException.class, () -> { 16 | // code-under-test 17 | }); 18 | assertThat(actual.getMessage(), is("...")); 19 | ``` 20 | 1. The `@Test` annotation no longer has an `expected` element 21 | 1. The [limited support for JUnit4 rules on JUnit5](http://junit.org/junit5/docs/snapshot/user-guide/#migrating-from-junit4-rule-support) does support the `ExpectedException` rule but this is an experimental feature. 22 | 23 | This extension offers much the same features as JUnit4's `ExpectedException` rule and is fully compatible with JUnit Jupiter's extension model. 24 | 25 | #### Usage 26 | 27 | This extension is engaged by adding the `@ExpectedException` annotation to a test method. This annotation allows you to declare: 28 | 29 | - The type of the exception which you expect to be thrown by this test method 30 | - A description of the message which you expect to be in the exception thrown by this test method. Unlike JUnit4's `ExpectedException` rule the message expectation does not accept a Hamcrest matcher so it is not quite as expressive as the equivalent JUnit4 rule however three different message matching strategies are supported: 31 | 32 | - `messageIs`: asserts that the exception message exactly matches the value you supplied 33 | - `messageStartsWith`: asserts that the exception message starts with the value you supplied 34 | - `messageContains`: asserts that the exception message contains the value you supplied 35 | 36 | Note: these are all case sensitive. 37 | 38 | #### Examples 39 | 40 | ###### Expect a Throwable, with no expectation on the exception message 41 | 42 | ``` 43 | @Test 44 | @ExpectedException(type = Throwable.class) 45 | public void canHandleAThrowable() throws Throwable { 46 | throw new Throwable("..."); 47 | } 48 | ``` 49 | ###### Expect a Throwable with an exact match on the exception message 50 | 51 | ``` 52 | @Test 53 | @ExpectedException(type = Throwable.class, messageIs = "Boom!") 54 | public void canHandleAThrowable() throws Throwable { 55 | throw new Throwable("Boom!"); 56 | } 57 | ``` 58 | 59 | ###### Expect a RuntimeException with an match on the beginning of the exception message 60 | 61 | ``` 62 | @Test 63 | @ExpectedException(type = RuntimeException.class, messageStartsWith = "Bye") 64 | public void canHandleAnExceptionWithAMessageWhichStartsWith() { 65 | throw new RuntimeException("Bye bye"); 66 | } 67 | ``` 68 | 69 | ###### Expect a custom exception type with an match on any part of the exception message 70 | 71 | ``` 72 | @Test 73 | @ExpectedException(type = MyDomainException.class, messageContains = "sorry") 74 | public void canHandleAnExceptionWithAMessageWhichContains() { 75 | throw new MyDomainException("Terribly sorry old chap"); 76 | } 77 | ``` 78 | 79 | Notes: 80 | 81 | Since usage of this extension implies that the developer _expects_ an exception to be thrown the following test case will fail since it throws no exception: 82 | 83 | ``` 84 | @Test 85 | @ExpectedException(type = Throwable.class) 86 | public void failsTestForMissingException() {} 87 | ``` 88 | 89 | The expected exception type will match on the given type and on any subclasses of that type. In other words, the following test will pass: 90 | 91 | ``` 92 | @Test 93 | @ExpectedException(type = Throwable.class, messageIs = "Boom!") 94 | public void canHandleAThrowable() throws Throwable { 95 | throw new Exception("Boom!"); 96 | } 97 | ``` 98 | 99 | This is for consistency with JUnit Jupiter, in which `AssertThrows` matches an exception type or any subclass of that exception type. 100 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 | 5 | Introduction 6 | ====== 7 | 8 | Provides the following features, implemented using [JUnit Jupiter's Extension Model](http://junit.org/junit5/docs/current/user-guide/#extensions): 9 | 10 | - `BenchmarkExtension`: publishes test elapsed time to the console, by default, but also available via the `EngineExecutionListener` for customised reporting 11 | 12 | - `ExpectedExceptionExtension`: allows you to run a test method with an expected exception and (optionally) exception message, delegating responsibility for making the assertion to the extension 13 | 14 | - `RandomBeansExtension`: allows you to inject random instances of classes into your tests, useful when you need a class instance to test with but you don't care about its contents 15 | 16 | - `SystemPropertyExtension`: allows you to set system properties before test execution and reverts these changes on test completion 17 | 18 | - `TemporaryFolderExtension`: allows you to create temporary files and directories in your tests, any such files or directories created in your tests are removed for you when the tests complete 19 | 20 | - `TestNameExtension`: makes the current test name available inside test methods 21 | 22 | - `WatcherExtension`: logs test execution flow including entry, exit and elapsed time in milliseconds 23 | 24 | #### Further Reading 25 | 26 | Follow the links in the side bar to read about each of these extensions. 27 | 28 | -------------------------------------------------------------------------------- /docs/randomBeans.md: -------------------------------------------------------------------------------- 1 | RandomBeansExtension 2 | ====== 3 | 4 | > With thanks to the author of [Random Beans](https://github.com/benas/random-beans). 5 | 6 | Sometimes you don't care about the specific value of a class-under-test (or a class involved in a test) you just need it to be populated with _something_. The `RandomBeansExtension` wraps the [Random Beans](https://github.com/benas/random-beans) fake data generator in a JUnit Jupiter extension allowing you to inject 'fake' instances of classes, primitives, collections into your test cases. The extension supports injection of test class fields and test method parameters. 7 | 8 | #### Usage 9 | 10 | This extension is engaged by adding the `@Random` annotation to a test class field or a test method parameter. This annotation allows you to declare: 11 | 12 | - `excludes`: fields to be excluded from the generated object, this supports _dot notation_ for fields in nested objects 13 | - `size`: for collection types, the size of the generated collection 14 | - `type`: for collection types, the underlying type of a generic collection 15 | 16 | You can use `@Random` annotation to a `static` field. 17 | From **v2.6.0** `static` field will be populated only once. Note, that in case of any default value (except `null`) the value will not be overridden by the extension. 18 | 19 | #### Examples 20 | 21 | ###### Test Class Fields 22 | 23 | ``` 24 | @ExtendWith(RandomBeansExtension.class) 25 | public class MyTest { 26 | 27 | // injected with a random String 28 | @Random private String anyString; 29 | 30 | @Random private static String anyStaticString; 31 | 32 | // injected with a random, fully populated instance of DomainObject 33 | @Random private DomainObject fullyPopulatedDomainObject; 34 | 35 | // injected with a random, partially populated instance of DomainObject 36 | @Random(excludes = {"wotsits", "id", "nestedDomainObject.address"}) 37 | private DomainObject partiallyPopulatedDomainObject; 38 | 39 | // injected with a List of random strings 40 | @Random(type = String.class) 41 | private List anyList; 42 | 43 | // injected with a List having 5 random strings 44 | @Random(size = 5, type = String.class) 45 | private List anyListOfSpecificSize; 46 | 47 | // injected with a Set of random strings 48 | @Random(type = String.class) 49 | private Set anySet; 50 | 51 | // injected with a Stream of random strings 52 | @Random(type = String.class) 53 | private Stream anyStream; 54 | 55 | // injected with a Collection of random strings 56 | @Random(type = String.class) 57 | private Collection anyCollection; 58 | 59 | // injected with a List having 2 fully populated DomainObject instances 60 | @Random(size = 2, type = DomainObject.class) 61 | private List anyFullyPopulatedDomainObjects; 62 | 63 | // injected with a List having 2 partially populated DomainObject instances 64 | @Random( 65 | size = 2, 66 | type = DomainObject.class, 67 | excludes = {"wotsits", "id", "nestedDomainObject.address"} 68 | ) 69 | private List anyPartiallyPopulatedDomainObjects; 70 | 71 | // ---- test methods which use these fields follow here ... 72 | } 73 | ``` 74 | 75 | ###### Test Method Parameters 76 | 77 | ``` 78 | @ExtendWith(RandomBeansExtension.class) 79 | public class RandomBeansExtensionParameterTest { 80 | 81 | @Test 82 | @ExtendWith(RandomBeansExtension.class) 83 | public void canInjectARandomString(@Random String anyString) { 84 | // supplied with a random String 85 | } 86 | 87 | @Test 88 | public void canInjectAFullyPopulatedRandomObject(@Random DomainObject domainObject) { 89 | // supplied with a random, fully populated DomainObject 90 | } 91 | 92 | @Test 93 | public void canInjectAPartiallyPopulatedRandomObject( 94 | @Random(excludes = {"wotsits", "id", "nestedDomainObject.address"}) 95 | DomainObject domainObject) { 96 | // supplied with a random, partially populated DomainObject 97 | } 98 | 99 | @Test 100 | public void canInjectARandomListOfDefaultSize(@Random(type = String.class) List anyList) 101 | throws Exception { 102 | // supplied with a List of random strings 103 | } 104 | 105 | @Test 106 | public void canInjectARandomListOfSpecificSize( 107 | @Random(size = 5, type = String.class) List anyListOfSpecificSize) { 108 | // supplied with a List containing 5 random strings 109 | } 110 | 111 | @Test 112 | public void canInjectARandomSet(@Random(type = String.class) Set anySet) 113 | throws Exception { 114 | // supplied with a Set of random strings 115 | } 116 | 117 | @Test 118 | public void canInjectARandomStream(@Random(type = String.class) Stream anyStream) 119 | throws Exception { 120 | // supplied with a Stream of random strings 121 | } 122 | 123 | @Test 124 | public void canInjectARandomCollection( 125 | @Random(type = String.class) Collection anyCollection) throws Exception { 126 | // supplied with a Collection of random strings 127 | } 128 | 129 | @Test 130 | public void canInjectRandomFullyPopulatedDomainObjects( 131 | @Random(size = 2, type = DomainObject.class) 132 | List anyFullyPopulatedDomainObjects) { 133 | // supplied with a List of 2 fully populated instances of DomainObject 134 | } 135 | 136 | @Test 137 | public void canInjectRandomPartiallyPopulatedDomainObjects( 138 | @Random( 139 | size = 2, 140 | type = DomainObject.class, 141 | excludes = {"wotsits", "id", "nestedDomainObject.address"} 142 | ) 143 | List anyPartiallyPopulatedDomainObjects) { 144 | // supplied with a List of 2 partially populated instances of DomainObject 145 | } 146 | } 147 | ``` 148 | 149 | ##### Overriding the Default Randomization Parameters 150 | 151 | In the event that the default [randomization parameters](https://github.com/j-easy/easy-random/wiki/Randomization-parameters) declared by this extension are not sufficient/appropriate for your use, you can override the defaults by using `@RegisterExtension` to pass in your own instance of RandomBeans' `EnhancedRandom`. For example: 152 | 153 | ``` 154 | public class RandomBeansExtensionProgrammaticRegistrationTest { 155 | 156 | static EnhancedRandom enhancedRandom = EnhancedRandomBuilder 157 | .aNewEnhancedRandomBuilder() 158 | .exclude(FieldDefinitionBuilder 159 | .field() 160 | .named("wotsits") 161 | .ofType(List.class) 162 | .inClass(DomainObject.class) 163 | .get()) 164 | .randomize(Integer.class, IntegerRangeRandomizer.aNewIntegerRangeRandomizer(0, 10)) 165 | .randomize(String.class, StringRandomizer.aNewStringRandomizer(5)) 166 | .randomize(Double.class, DoubleRangeRandomizer.aNewDoubleRangeRandomizer(0.0, 10.0)) 167 | .build(); 168 | 169 | 170 | @RegisterExtension 171 | static RandomBeansExtension randomBeansExtension = new RandomBeansExtension(enhancedRandom); 172 | 173 | @Test 174 | public void canInjectIntegerInRangeOf( 175 | @Random(type = Integer.class) Integer anyInteger) throws Exception { 176 | assertThat(anyInteger, notNullValue()); 177 | assertThat(anyInteger, lessThanOrEqualTo(10)); 178 | assertThat(anyInteger, greaterThanOrEqualTo(0)); 179 | } 180 | 181 | @Test 182 | public void canInjectDoubleInRangeOf( 183 | @Random(type = Double.class) Double anyDouble) throws Exception { 184 | assertThat(anyDouble, notNullValue()); 185 | assertThat(anyDouble, lessThanOrEqualTo(10.0)); 186 | assertThat(anyDouble, greaterThanOrEqualTo(0.0)); 187 | } 188 | } 189 | ``` -------------------------------------------------------------------------------- /docs/systemProperty.md: -------------------------------------------------------------------------------- 1 | SystemPropertyExtension 2 | ====== 3 | 4 | If your test relies on system properties you could set them and unset them in 'before' and 'after' lifecycle methods. In Junit5, setting system properties for all tests in a test case might look like this: 5 | 6 | ``` 7 | @BeforeAll 8 | public static void setSystemProperties() { 9 | // set the system properties 10 | // ... 11 | } 12 | 13 | @AfterAll 14 | public static void unsetSystemProperties() { 15 | // unset the system properties 16 | // and reinstate the original value for any property which was overwritten 17 | // ... 18 | } 19 | ``` 20 | 21 | Setting system properties for a single test in a test case might look like this: 22 | 23 | ``` 24 | @Test 25 | public void aTest() { 26 | // set the system properties 27 | 28 | try { 29 | // code-under-test 30 | } finally { 31 | // unset the system properties 32 | // and reinstate the original value for any property which was overwritten 33 | } 34 | } 35 | ``` 36 | 37 | These approaches will work but they are verbose and brittle. The `SystemPropertyExtension` allows you to _declare_ this behaviour by adding the `@SystemProperty` annotation to a test case or a test method. This annotation allows you to declare: 38 | 39 | - `name`: the system property name 40 | - `value`: The system property value 41 | 42 | #### Examples 43 | 44 | ###### Class Level System Property 45 | 46 | ``` 47 | @SystemProperty(name = "x", value = "y") 48 | public class MyTest { 49 | 50 | @Test 51 | public void aTest() { 52 | assertThat(System.getProperty("x"), is("y")); 53 | } 54 | } 55 | ``` 56 | 57 | ###### Class Level System Properties 58 | 59 | ``` 60 | @SystemProperty(name = "x", value = "y") 61 | @SystemProperty(name = "p", value = "q") 62 | public class MyTest { 63 | 64 | @Test 65 | public void aTest() { 66 | assertThat(System.getProperty("x"), is("y")); 67 | assertThat(System.getProperty("p"), is("q")); 68 | } 69 | } 70 | ``` 71 | 72 | ###### Method Level System Property 73 | 74 | ``` 75 | public class MyTest { 76 | 77 | @Test 78 | @SystemProperty(name = "x", value = "y") 79 | public void aTes() { 80 | assertThat(System.getProperty("x"), is("y")); 81 | } 82 | } 83 | ``` 84 | 85 | ###### Method Level System Properties 86 | 87 | ``` 88 | public class MyTest { 89 | 90 | @Test 91 | @SystemProperty(name = "x", value = "y") 92 | @SystemProperty(name = "p", value = "q") 93 | public void aTes() { 94 | assertThat(System.getProperty("x"), is("y")); 95 | assertThat(System.getProperty("p"), is("q")); 96 | } 97 | } 98 | ``` -------------------------------------------------------------------------------- /docs/temporaryFolder.md: -------------------------------------------------------------------------------- 1 | TemporaryFolderExtension 2 | ====== 3 | 4 | The JUnit 4 `TemporaryFolder` rule allowed for the creation of files and folders that are deleted when the test method finished (whether the test method passed or not). By default no exception was thrown if the file system resources could not be deleted. 5 | 6 | This extension offers the same features as JUnit4's `TemporaryFolder` rule and is fully compatible with JUnit Jupiter. 7 | 8 | #### Usage 9 | 10 | This extension is engaged by adding the `@ExtendWith` annotation to a test class or a test method. This annotation results in a `TemporaryFolder` instance being injected into the test case or test method. You can then invoke methods on `TemporaryFolder` to create files or directories for use by your test(s). 11 | 12 | #### Examples 13 | 14 | ###### Instance Variable TemporaryFolder 15 | 16 | ``` 17 | @ExtendWith(TemporaryFolderExtension.class) 18 | public class MyTest { 19 | 20 | private TemporaryFolder temporaryFolder; 21 | 22 | @BeforeEach 23 | public void prepare(TemporaryFolder temporaryFolder) { 24 | this.temporaryFolder = temporaryFolder; 25 | } 26 | 27 | @Test 28 | public void canUseTemporaryFolder() throws IOException { 29 | // use the temporary folder itself 30 | File root = temporaryFolder.getRoot(); 31 | 32 | // create a file within the temporary folder 33 | File file = temporaryFolder.createFile("foo.txt"); 34 | assertThat(file.exists(), is(true)); 35 | 36 | // create a directory within the temporary folder 37 | File dir = temporaryFolder.createDirectory("bar"); 38 | assertThat(dir.exists(), is(true)); 39 | } 40 | } 41 | ``` 42 | 43 | ###### Method Level TemporaryFolder 44 | 45 | ``` 46 | public class MyTest { 47 | 48 | @Test 49 | @ExtendWith(TemporaryFolderExtension.class) 50 | public void canUseTemporaryFolder(TemporaryFolder temporaryFolder) throws IOException { 51 | // use the temporary folder itself 52 | File root = temporaryFolder.getRoot(); 53 | 54 | // create a file within the temporary folder 55 | File file = temporaryFolder.createFile("foo.txt"); 56 | assertThat(file.exists(), is(true)); 57 | 58 | // create a directory within the temporary folder 59 | File dir = temporaryFolder.createDirectory("bar"); 60 | assertThat(dir.exists(), is(true)); 61 | } 62 | } 63 | ``` 64 | 65 | ###### Class Variable TemporaryFolder 66 | 67 | ``` 68 | @ExtendWith(TemporaryFolderExtension.class) 69 | public class MyTest { 70 | 71 | private static TemporaryFolder TEMPORARY_FOLDER; 72 | 73 | @BeforeAll 74 | public void prepare(TemporaryFolder givenTemporaryFolder) { 75 | this.TEMPORARY_FOLDER = givenTemporaryFolder; 76 | } 77 | 78 | @Test 79 | public void canUseTemporaryFolder() throws IOException { 80 | // use the temporary folder itself 81 | File root = TEMPORARY_FOLDER.getRoot(); 82 | 83 | // create a file within the temporary folder 84 | File file = TEMPORARY_FOLDER.createFile("foo.txt"); 85 | assertThat(file.exists(), is(true)); 86 | 87 | // create a directory within the temporary folder 88 | File dir = TEMPORARY_FOLDER.createDirectory("bar"); 89 | assertThat(dir.exists(), is(true)); 90 | } 91 | } 92 | ``` -------------------------------------------------------------------------------- /docs/testName.md: -------------------------------------------------------------------------------- 1 | WatcherExtension 2 | ====== 3 | 4 | Similar to JUnit4's `TestName` Rule, this extension makes the current test name available inside test methods. 5 | 6 | #### Usage 7 | 8 | This extension is engaged by adding the `@ExtendWith(TestNameExtension.class)` annotation to a test class. 9 | 10 | #### Example 11 | 12 | ``` 13 | @ExtendWith(TestNameExtension.class) 14 | public class MyTest { 15 | 16 | @TestName 17 | private String testName; 18 | 19 | @Test 20 | public void someTest() { 21 | // use the populated testName 22 | // ... 23 | } 24 | } 25 | ``` -------------------------------------------------------------------------------- /docs/watcher.md: -------------------------------------------------------------------------------- 1 | WatcherExtension 2 | ====== 3 | 4 | Similar to JUnit4's `TestWatcher` this extension watches the test invocation without modifying it or interfering with it in any way. Instead, it just logs: 5 | 6 | 1. Test method entry 7 | 1. Test method exit 8 | 1. Test elapsed time in milliseconds 9 | 10 | This extension uses `java.util.logging.Logger` so it will either play nicely with your current logging solution or there will be a bridge to allow it to do so (e.g. [jul-to-slf4j](https://www.slf4j.org/legacy.html)). 11 | 12 | #### Usage 13 | 14 | This extension is engaged by adding the `@ExtendWith(WatcherExtension.class)` annotation to a test class. 15 | 16 | #### Example 17 | 18 | ``` 19 | @ExtendWith(WatcherExtension.class) 20 | public class MyTest { 21 | 22 | // entry, exit and elapsed time will be logged for every test in this test case 23 | 24 | } 25 | ``` 26 | 27 | #### Output 28 | 29 | Example output (after bridging to SLF4J): 30 | 31 | ``` 32 | 2017-11-15 17:17:03,945|[main]|INFO |o.g.j.e.watcher.WatcherExtension|Starting test [canGet] 33 | 2017-11-15 17:17:04,167|[main]|INFO |o.g.d.http.okhttp.OkHttpClient|Get from: http://host:1234/some/end/point 34 | 2017-11-15 17:17:04,201|[main]|INFO |o.g.j.e.watcher.WatcherExtension|Completed test [canGet] in 247ms 35 | ``` -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | io.github.glytching 6 | junit-extensions 7 | 2.7.0-SNAPSHOT 8 | jar 9 | 10 | JUnit Extensions 11 | 12 | JUnit Jupiter extensions, providing 'JUnit5 replacements' for some common JUnit4 Rules. 13 | 14 | https://github.com/glytching/junit-extensions 15 | 16 | 17 | git@github.com:glytching/junit-extensions.git 18 | scm:git:git@github.com:glytching/junit-extensions.git 19 | scm:git:git@github.com:glytching/junit-extensions.git 20 | HEAD 21 | 22 | 23 | 24 | Travis CI 25 | https://travis-ci.org/glytching/junit-extensions 26 | 27 | 28 | 29 | GitHub 30 | https://github.com/glytching/junit-extensions/issues 31 | 32 | 33 | 34 | 35 | The Apache License, Version 2.0 36 | http://www.apache.org/licenses/LICENSE-2.0.txt 37 | 38 | 39 | 40 | 41 | 42 | glytching 43 | glytching 44 | https://github.com/glytching 45 | 46 | Lead developer 47 | 48 | 49 | 50 | 51 | 52 | 1.8 53 | 1.8 54 | UTF-8 55 | 56 | 1.2.0 57 | 5.2.0 58 | 1.3 59 | 2.7.19 60 | 3.9.0 61 | 62 | 2.22.0 63 | 3.0.2 64 | 3.0.0-M1 65 | 4.3.0 66 | 0.7.9 67 | 3.0.0 68 | 69 | 70 | 71 | 72 | ossrh 73 | https://oss.sonatype.org/content/repositories/snapshots 74 | 75 | 76 | ossrh 77 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 78 | 79 | 80 | 81 | 82 | 83 | 84 | org.apache.maven.plugins 85 | maven-jar-plugin 86 | ${maven.jar.plugin.version} 87 | 88 | 89 | 90 | glytching 91 | 92 | 93 | 94 | 95 | 96 | org.apache.maven.plugins 97 | maven-source-plugin 98 | ${maven.source.plugin.version} 99 | 100 | 101 | attach-sources 102 | 103 | jar-no-fork 104 | 105 | 106 | 107 | 108 | 109 | org.apache.maven.plugins 110 | maven-javadoc-plugin 111 | ${maven.javadoc.plugin.version} 112 | 113 | 114 | attach-javadocs 115 | 116 | jar 117 | 118 | 119 | 120 | 121 | glytching 122 | 123 | 124 | 125 | 126 | 127 | 128 | -Xdoclint:none 129 | 130 | 131 | 132 | 133 | 134 | maven-surefire-plugin 135 | ${maven.surefire.plugin.version} 136 | 137 | 138 | org.junit.platform 139 | junit-platform-surefire-provider 140 | ${junit.platform.version} 141 | 142 | 143 | org.junit.jupiter 144 | junit-jupiter-engine 145 | ${junit.jupiter.version} 146 | 147 | 148 | 149 | 150 | 151 | 152 | org.jacoco 153 | jacoco-maven-plugin 154 | ${maven.jacoco.plugin.version} 155 | 156 | 157 | prepare-agent 158 | 159 | prepare-agent 160 | 161 | 162 | 163 | 164 | 165 | org.eluder.coveralls 166 | coveralls-maven-plugin 167 | ${maven.coveralls.plugin.version} 168 | 169 | true 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | org.junit.jupiter 178 | junit-jupiter-api 179 | ${junit.jupiter.version} 180 | 181 | 182 | io.github.benas 183 | random-beans 184 | ${random.beans.version} 185 | 186 | 187 | 188 | org.junit.jupiter 189 | junit-jupiter-engine 190 | ${junit.jupiter.version} 191 | test 192 | 193 | 194 | org.junit.platform 195 | junit-platform-launcher 196 | ${junit.platform.version} 197 | test 198 | 199 | 200 | org.junit.platform 201 | junit-platform-runner 202 | ${junit.platform.version} 203 | test 204 | 205 | 206 | 207 | junit 208 | junit 209 | 210 | 211 | 212 | 213 | org.hamcrest 214 | hamcrest-library 215 | ${hamcrest.version} 216 | test 217 | 218 | 219 | org.mockito 220 | mockito-core 221 | ${mockito.version} 222 | test 223 | 224 | 225 | 226 | 227 | 228 | 229 | publish 230 | 231 | false 232 | 233 | 234 | gpg 235 | 236 | 237 | 238 | 239 | org.apache.maven.plugins 240 | maven-gpg-plugin 241 | 1.6 242 | 243 | 244 | sign-artifacts 245 | verify 246 | 247 | sign 248 | 249 | 250 | 251 | 252 | 253 | org.sonatype.plugins 254 | nexus-staging-maven-plugin 255 | 1.6.7 256 | true 257 | 258 | ossrh 259 | https://oss.sonatype.org/ 260 | true 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | JUnit Extensions 2 | ==== 3 | 4 | [![Build Status](https://circleci.com/gh/glytching/junit-extensions.svg?style=svg)](https://circleci.com/gh/glytching/junit-extensions) [![Coverage Status](https://coveralls.io/repos/github/glytching/junit-extensions/badge.svg?branch=master)](https://coveralls.io/github/glytching/junit-extensions?branch=master) [![Scrutinizer](https://img.shields.io/scrutinizer/g/glytching/junit-extensions.svg)](https://scrutinizer-ci.com/g/glytching/junit-extensions/) [![Javadoc](https://javadoc.io/badge2/io.github.glytching/junit-extensions/javadoc.svg)](https://javadoc.io/doc/io.github.glytching/junit-extensions) [![Maven Central](https://img.shields.io/maven-central/v/io.github.glytching/junit-extensions.svg)](https://repo1.maven.org/maven2/io/github/glytching/junit-extensions/2.6.0/) [![GitHub Release](https://img.shields.io/github/release/glytching/junit-extensions.svg)](https://github.com/glytching/junit-extensions/releases) 5 | 6 | > With thanks and appreciation to the authors of [JUnit5](https://github.com/junit-team/junit5/graphs/contributors). 7 | 8 | There have been discussions amongst the JUnit community (see [this](https://github.com/junit-team/junit5/issues/169) and [this](https://github.com/junit-team/junit5-samples/issues/4)) about providing official [JUnit Jupiter Extensions](http://junit.org/junit5/docs/current/user-guide/#extensions) for the most popular [JUnit4 Rules](https://github.com/junit-team/junit4/wiki/Rules). The upshot of these discussions seems to be enabling [limited support for JUnit4 rules on JUnit5](http://junit.org/junit5/docs/snapshot/user-guide/#migrating-from-junit4-rule-support). For anyone who would rather move entirely to JUnit5, this library provides JUnit5 implementations of some of the commonly used JUnit4 rules. 9 | 10 | ### Documentation 11 | 12 | - [User Guide](https://glytching.github.io/junit-extensions/) 13 | - [Javadocs](http://www.javadoc.io/doc/io.github.glytching/junit-extensions) 14 | 15 | ### Extensions 16 | 17 | - `BenchmarkExtension`: publishes test elapsed time to the console, by default, but also available via the `EngineExecutionListener` for customised reporting. 18 | - `ExpectedExceptionExtension`: allows you to run a test method with an expected exception and (optionally) exception message, delegating responsibility for making the assertion to the extension. 19 | - `RandomBeansExtension`: allows you to inject random instances of classes into your tests, useful when you need a class instance to test with but you don't care about its contents. 20 | - `SystemPropertyExtension`: allows you to set system properties before test execution and reverts these changes on test completion. 21 | - `TemporaryFolderExtension`: allows you to create temporary files and directories in your test, any such files or directories created in your tests are removed for you when the tests complete. 22 | - `TestNameExtension`: allows you to use the name of the currently executing test within your test cases. 23 | - `WatcherExtension`: logs test execution flow including entry, exit and elapsed time in milliseconds. 24 | 25 | ### Using JUnit-Extensions 26 | 27 | The `junit-extensions` library is available on [Maven Central](http://search.maven.org/#artifactdetails%7Cio.github.glytching%7Cjunit-extensions%7C2.6.0%7Cjar): 28 | 29 | #### Maven 30 | 31 | ``` 32 | 33 | io.github.glytching 34 | junit-extensions 35 | 2.6.0 36 | test 37 | 38 | ``` 39 | 40 | #### Gradle 41 | 42 | ``` 43 | testCompile 'io.github.glytching:junit-extensions:2.6.0' 44 | ``` 45 | 46 | ### Building JUnit-Extensions 47 | 48 | ``` 49 | $ git clone https://github.com/glytching/junit-extensions.git 50 | $ cd junit-extensions 51 | $ mvn clean install 52 | ``` 53 | 54 | This will compile and run all automated tests and install the library in your local Maven repository. 55 | 56 | Note: the code is formatted using the [Google Code Formatter](https://github.com/google/google-java-format). 57 | 58 | ### License 59 | 60 | Licensed under the Apache License, Version 2.0 (the "License"); 61 | you may not use this file except in compliance with the License. 62 | 63 | You may obtain a copy of the License at 64 | 65 | http://www.apache.org/licenses/LICENSE-2.0 66 | 67 | Unless required by applicable law or agreed to in writing, software 68 | distributed under the License is distributed on an "AS IS" BASIS, 69 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 70 | See the License for the specific language governing permissions and 71 | limitations under the License.### 72 | -------------------------------------------------------------------------------- /src/main/java/io/github/glytching/junit/extension/benchmark/BenchmarkExtension.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.github.glytching.junit.extension.benchmark; 18 | 19 | import org.junit.jupiter.api.extension.AfterTestExecutionCallback; 20 | import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; 21 | import org.junit.jupiter.api.extension.ExtensionContext; 22 | 23 | import java.lang.reflect.Method; 24 | import java.util.concurrent.TimeUnit; 25 | 26 | import static io.github.glytching.junit.extension.util.ExtensionUtil.getStore; 27 | 28 | /** 29 | * The benchmark extension publishes elapsed time to the execution listener. By default, this 30 | * produces output like so: 31 | * 32 | *
 33 |  * timestamp = 2018-08-30T16:28:47.701, Elapsed time in MICROSECONDS for canBenchmark = 13213
 34 |  * 
35 | * 36 | * Your own implementation of the {@code EngineExecutionListener} could adopt a different template 37 | * for the event string or it could collect and aggregate results for all tests in a test case or it 38 | * could write results to somewhere other than the console etc. 39 | * 40 | *

By default, elapsed times are reported in {@link TimeUnit#MILLISECONDS} but you can use {@link 41 | * org.junit.jupiter.api.extension.RegisterExtension} to choose a different {@link TimeUnit}. 42 | * 43 | *

Usage example: 44 | * 45 | *

 46 |  * @ExtendWith(BenchmarkExtension.class)
 47 |  * public class MyTest {
 48 |  *
 49 |  *  @Test
 50 |  *  public void aTest() {
 51 |  *      // ...
 52 |  *  }
 53 |  * }
 54 |  * 
55 | * 56 | *
 57 |  * public class MyTest {
 58 |  *
 59 |  *  // report elapsed times in a non default time unit
 60 |  *  @@RegisterExtension
 61 |  *  static BenchmarkExtension benchmarkExtension = new BenchmarkExtension(TimeUnit.MICROSECONDS);
 62 |  *
 63 |  *  @Test
 64 |  *  public void aTest() {
 65 |  *      // ...
 66 |  *  }
 67 |  * }
 68 |  * 
69 | * 70 | * @since 2.4.0 71 | */ 72 | public class BenchmarkExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback { 73 | 74 | public static final String REPORT_EVENT_FORMAT = "Elapsed time in %s for %s"; 75 | 76 | private final TimeUnit timeUnit; 77 | 78 | /** Constructs an instance of this class which will report using the default time unit. */ 79 | @SuppressWarnings("unused") 80 | public BenchmarkExtension() { 81 | this(TimeUnit.MILLISECONDS); 82 | } 83 | 84 | /** 85 | * Constructs an instance of this class which will report using the given {@code timeUnit}. 86 | * 87 | * @param timeUnit the time unit in which benchmarks will be reported 88 | */ 89 | public BenchmarkExtension(TimeUnit timeUnit) { 90 | this.timeUnit = timeUnit; 91 | } 92 | 93 | /** 94 | * Store a {@link StopWatch} instance against the test method name for use in {@link 95 | * #afterTestExecution(ExtensionContext)}. 96 | * 97 | * @param extensionContext the context in which the current test or container is being 98 | * executed 99 | * @throws Exception 100 | */ 101 | @Override 102 | public void beforeTestExecution(ExtensionContext extensionContext) throws Exception { 103 | // put a StopWatch in the context for the current test invocation 104 | getStore(extensionContext, this.getClass()) 105 | .put(extensionContext.getRequiredTestMethod(), new StopWatch()); 106 | } 107 | 108 | /** 109 | * Gather the elapsed time, using {@link StopWatch} stored by {@link 110 | * #beforeTestExecution(ExtensionContext)}. 111 | * 112 | * @param extensionContext the context in which the current test or container is being 113 | * executed 114 | * @throws Exception 115 | */ 116 | @Override 117 | public void afterTestExecution(ExtensionContext extensionContext) throws Exception { 118 | Method testMethod = extensionContext.getRequiredTestMethod(); 119 | 120 | // get the StopWatch from the context for the current test invocation and report on it 121 | long duration = 122 | getStore(extensionContext, this.getClass()) 123 | .get(testMethod, StopWatch.class) 124 | .duration(timeUnit); 125 | 126 | extensionContext.publishReportEntry( 127 | String.format(REPORT_EVENT_FORMAT, timeUnit.name(), testMethod.getName()), 128 | Long.toString(duration)); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/main/java/io/github/glytching/junit/extension/benchmark/StopWatch.java: -------------------------------------------------------------------------------- 1 | package io.github.glytching.junit.extension.benchmark; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | 5 | import static java.lang.System.nanoTime; 6 | import static java.util.concurrent.TimeUnit.NANOSECONDS; 7 | 8 | /** A simple wrapper for use in reporting test elapsed time in a chosen {@link TimeUnit}. */ 9 | public class StopWatch { 10 | 11 | private long start; 12 | 13 | /** Constructs a StopWatch with a start time equal to the system's current nano time. */ 14 | public StopWatch() { 15 | this.start = nanoTime(); 16 | } 17 | 18 | /** 19 | * Returns the duration of since this instance was created. The duration will be converted into 20 | * the given {@code timeUnit}. 21 | * 22 | * @param timeUnit the units in which the duration is returned 23 | * @return The elapsed time converted to the specified units 24 | */ 25 | public long duration(TimeUnit timeUnit) { 26 | return timeUnit.convert(nanoTime() - start, NANOSECONDS); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/io/github/glytching/junit/extension/exception/ExpectedException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.github.glytching.junit.extension.exception; 18 | 19 | import org.junit.jupiter.api.extension.ExtendWith; 20 | 21 | import java.lang.annotation.*; 22 | 23 | /** 24 | * Describes the expectations for an exception, including: 25 | * 26 | *
    27 | *
  • The exception class 28 | *
  • Case sensitive matchers on the exception message in the form of: 29 | *
      30 | *
    • Message matches exactly 31 | *
    • Message starts with 32 | *
    • Message contains 33 | *
    34 | *
35 | * 36 | *

Usage example: 37 | * 38 | *

39 |  *  // match an exception of type Exception containing the message "Boom!"
40 |  *  @ExpectedException(type = RuntimeException.class, messageIs = "Boom!")
41 |  *
42 |  *  // match an exception of type RuntimeException containing a message which starts with "Bye"
43 |  *  @ExpectedException(type = RuntimeException.class, messageStartsWith = "Bye")
44 |  *
45 |  *  // match an exception of type MyDomainException having a message which contains "ouch"
46 |  *  @ExpectedException(type = MyDomainException.class, messageContains = "ouch")
47 |  * 
48 | */ 49 | @Retention(RetentionPolicy.RUNTIME) 50 | @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) 51 | @Documented 52 | @ExtendWith(ExpectedExceptionExtension.class) 53 | public @interface ExpectedException { 54 | 55 | Class type(); 56 | 57 | String messageIs() default ""; 58 | 59 | String messageStartsWith() default ""; 60 | 61 | String messageContains() default ""; 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/io/github/glytching/junit/extension/exception/ExpectedExceptionExtension.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.github.glytching.junit.extension.exception; 18 | 19 | import org.junit.jupiter.api.Assertions; 20 | import org.junit.jupiter.api.extension.AfterTestExecutionCallback; 21 | import org.junit.jupiter.api.extension.ExtensionContext; 22 | import org.junit.jupiter.api.extension.TestExecutionExceptionHandler; 23 | 24 | import java.util.Optional; 25 | import java.util.function.Function; 26 | import java.util.function.Predicate; 27 | 28 | import static io.github.glytching.junit.extension.util.ExtensionUtil.getStore; 29 | import static org.junit.platform.commons.util.AnnotationUtils.findAnnotation; 30 | import static org.junit.platform.commons.util.FunctionUtils.where; 31 | 32 | /** 33 | * The expected exception extension allows the developer to express the expectations of an exception 34 | * to be thrown by the code-under-test. This extension is engaged by adding the {@link 35 | * ExpectedException} annotation to a test method. 36 | * 37 | *

Usage example: 38 | * 39 | *

 40 |  * public class MyTest {
 41 |  *
 42 |  *     @Test
 43 |  *     @ExpectedException(type = Exception.class, messageIs = "Boom!")
 44 |  *     public void canHandleAnException() throws Exception {
 45 |  *         // ...
 46 |  *         throw new Exception("Boom!");
 47 |  *     }
 48 |  *
 49 |  *
 50 |  *     @Test
 51 |  *     @ExpectedException(type = RuntimeException.class, messageStartsWith = "Bye")
 52 |  *     public void canHandleAnExceptionWithAMessageWhichStartsWith() {
 53 |  *         // ...
 54 |  *         throw new RuntimeException("Bye bye");
 55 |  *     }
 56 |  * }
 57 |  * 
58 | * 59 | * Notes: 60 | * 61 | *
    62 | *
  • Since usage of this extension implies that the developer expects an exception to be 63 | * thrown the following test case will fail since it throws no exception: 64 | *
     65 |  *   @Test
     66 |  *   @ExpectedException(type = Throwable.class)
     67 |  *   public void failsTestForMissingException() {}
     68 |  * 
    69 | * This is to avoid a false positive where a test is declared to expect an exception and 70 | * passes even if no exception is thrown. 71 | *
  • The expected exception type will match on the given type and any subclasses of that type. 72 | * In other words, the following test will pass: 73 | *
     74 |  *          @Test
     75 |  *          @ExpectedException(type = Throwable.class, messageIs = "Boom!")
     76 |  *          public void canHandleAThrowable() throws Throwable {
     77 |  *              throw new Exception("Boom!");
     78 |  *          }
     79 |  *       
    80 | * This is for consistency with JUnit Jupiter, in which AssertThrows matches an 81 | * exception type or any subclass of that exception type. 82 | *
83 | * 84 | * @see JUnit 4 85 | * ExpectedException Rule 86 | * @since 1.0.0 87 | */ 88 | public class ExpectedExceptionExtension 89 | implements TestExecutionExceptionHandler, AfterTestExecutionCallback { 90 | 91 | private static final String KEY = "exceptionWasHandled"; 92 | 93 | private final Function function; 94 | 95 | public ExpectedExceptionExtension() { 96 | this.function = Throwable::getMessage; 97 | } 98 | 99 | /** 100 | * Handle the supplied {@code Throwable throwable}. If the {@code extensionContext} is annotated 101 | * with {@link ExpectedException} and if the {@code throwable} matches the expectations expressed 102 | * in the {@link ExpectedException} annotation then the supplied {@code throwable} is swallowed 103 | * otherwise the supplied {@code throwable} is rethrown. 104 | * 105 | * @param extensionContext the current extension context 106 | * @param throwable the {@code Throwable} to handle 107 | * @throws Throwable 108 | */ 109 | @Override 110 | public void handleTestExecutionException(ExtensionContext extensionContext, Throwable throwable) 111 | throws Throwable { 112 | Optional optional = 113 | findAnnotation(extensionContext.getTestMethod(), ExpectedException.class); 114 | if (optional.isPresent()) { 115 | 116 | ExpectedException annotation = optional.get(); 117 | // see https://github.com/glytching/junit-extensions/issues/5 118 | if (annotation.type().isAssignableFrom(throwable.getClass())) { 119 | if (where(function, getPredicate(annotation)).test(throwable)) { 120 | getStore(extensionContext, this.getClass()).put(KEY, true); 121 | 122 | // swallow the exception because the caller has declared it to be expected 123 | return; 124 | } 125 | } 126 | } 127 | throw throwable; 128 | } 129 | 130 | /** 131 | * The presence of {@link ExpectedException} on a test is a clear statement by the developer that 132 | * some exception must be thrown by that test. Therefore, if the invocation arrives here without 133 | * having been evaluated by {@link #handleTestExecutionException(ExtensionContext, Throwable)} and 134 | * with no exception in the context then this expectation has not been met and rather than failing 135 | * silently (and giving a false positive) the library will explicitly fail on this condition. 136 | * 137 | * @param extensionContext 138 | * @throws Exception 139 | */ 140 | @Override 141 | public void afterTestExecution(ExtensionContext extensionContext) throws Exception { 142 | Boolean exceptionWasHandled = 143 | (Boolean) getStore(extensionContext, this.getClass()).getOrComputeIfAbsent(KEY, s -> false); 144 | if (!exceptionWasHandled && !extensionContext.getExecutionException().isPresent()) { 145 | Assertions.fail("Expected an exception but no exception was thrown!"); 146 | } 147 | } 148 | 149 | /** 150 | * Maps the expectations expressed in the given {@code annotation} to a {@link Predicate}. 151 | * 152 | * @param annotation encapsulates the callers' expression of what's to be deemed an acceptable 153 | * exception 154 | * @return a predicate which can be used to assess whether an exception matches the expectations 155 | * expressed in the given {@code annotation} 156 | */ 157 | private Predicate getPredicate(ExpectedException annotation) { 158 | if (has(annotation.messageStartsWith())) { 159 | return s -> s.startsWith(annotation.messageStartsWith()); 160 | } else if (has(annotation.messageContains())) { 161 | return s -> s.contains(annotation.messageContains()); 162 | } else if (has(annotation.messageIs())) { 163 | return s -> s.equals(annotation.messageIs()); 164 | } else { 165 | // the default 166 | return s -> true; 167 | } 168 | } 169 | 170 | private boolean has(String incoming) { 171 | return incoming != null && incoming.length() > 0; 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/main/java/io/github/glytching/junit/extension/folder/TemporaryFolder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.github.glytching.junit.extension.folder; 18 | 19 | import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource; 20 | 21 | import java.io.File; 22 | import java.io.IOException; 23 | import java.nio.file.*; 24 | import java.nio.file.attribute.BasicFileAttributes; 25 | 26 | import static java.nio.file.FileVisitResult.CONTINUE; 27 | 28 | /** 29 | * Encapsulates the {@link #rootFolder} within which any files or directories will be created along 30 | * with the operations which a tester may wish to invoke ({@link #createFile(String)}, {@link 31 | * #createDirectory(String)}) and post test invocations which the associated extension will invoke. 32 | */ 33 | @SuppressWarnings({"ResultOfMethodCallIgnored", "nls"}) 34 | public class TemporaryFolder implements CloseableResource { 35 | private static final String FILE_PREFIX = "junit"; 36 | private static final String FILE_SUFFIX = ".tmp"; 37 | 38 | /** 39 | * The root folder within which any files or directories will be created, on {@link #destroy()} 40 | * this folder and all of its contents will be silently deleted. 41 | */ 42 | private final File rootFolder; 43 | 44 | /** 45 | * Package protected since a {@link TemporaryFolder}'s lifecycle is expected to be controlled by 46 | * its associated extension. 47 | */ 48 | TemporaryFolder() { 49 | try { 50 | // do not use Files.createTempFile to create a directory 51 | // see https://rules.sonarsource.com/java/RSPEC-2976 52 | Path tempPath = Files.createTempDirectory(FILE_PREFIX); 53 | rootFolder = tempPath.toFile(); 54 | } catch (IOException ex) { 55 | throw new TemporaryFolderException("Failed to prepare root folder!", ex); 56 | } 57 | } 58 | 59 | @Override 60 | public void close() throws Throwable { 61 | destroy(); 62 | } 63 | 64 | /** 65 | * Returns the root folder. Exposing this offers some back compatability with JUnit4's {@code 66 | * TemporaryFolder} so test cases which are used to invoking {@code getRoot()} on a JUnit4 rule 67 | * can adopt the same approach with this {@code TemporaryFolder}. In addition, this may be useful 68 | * where you want to use the root folder itself without creating files or directories within it. 69 | * 70 | *

Note: the extension is responsible for creating/managing/destroying the root folder 71 | * so don't bother trying to clean it up yourself and don't expect that anything you do to it will 72 | * survive post-test-cleanup. 73 | * 74 | * @see Issue 8 75 | * @return the root folder 76 | */ 77 | public File getRoot() { 78 | return rootFolder; 79 | } 80 | 81 | /** 82 | * Create a file within the temporary folder root. 83 | * 84 | * @param fileName the name of the file to be created 85 | * @return the newly created file instance 86 | * @throws IOException in case the file creation call fails 87 | */ 88 | public File createFile(String fileName) throws IOException { 89 | Path path = Paths.get(rootFolder.getPath(), fileName); 90 | return Files.createFile(path).toFile(); 91 | } 92 | 93 | /** 94 | * Create a directory within the temporary folder root. 95 | * 96 | * @param directoryName the name of the directory to be created 97 | * @return the directory instance 98 | */ 99 | public File createDirectory(String directoryName) { 100 | Path path = Paths.get(rootFolder.getPath(), directoryName); 101 | try { 102 | return Files.createDirectory(path).toFile(); 103 | } catch (IOException ex) { 104 | throw new TemporaryFolderException( 105 | String.format("Failed to create directory: '%s'", path.toString()), ex); 106 | } 107 | } 108 | 109 | /** 110 | * Deletes the {@link #rootFolder} and all of its contents. This is package protected because a 111 | * {@link TemporaryFolder}'s lifecycle is expected to be controlled by its associated extension. 112 | * 113 | *

Note: any exception encountered during deletion will be swallowed. 114 | */ 115 | void destroy() throws IOException { 116 | if (rootFolder.exists()) { 117 | // walk the contents deleting each 118 | Files.walkFileTree( 119 | rootFolder.toPath(), 120 | new SimpleFileVisitor() { 121 | @Override 122 | public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) 123 | throws IOException { 124 | return delete(file); 125 | } 126 | 127 | @Override 128 | public FileVisitResult postVisitDirectory(Path directory, IOException exception) 129 | throws IOException { 130 | return delete(directory); 131 | } 132 | 133 | @SuppressWarnings("SameReturnValue") 134 | private FileVisitResult delete(Path file) throws IOException { 135 | Files.delete(file); 136 | return CONTINUE; 137 | } 138 | }); 139 | 140 | if (rootFolder.exists()) { 141 | // delete the parent, if it still exists 142 | Files.delete(rootFolder.toPath()); 143 | } 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/main/java/io/github/glytching/junit/extension/folder/TemporaryFolderException.java: -------------------------------------------------------------------------------- 1 | package io.github.glytching.junit.extension.folder; 2 | 3 | public class TemporaryFolderException extends RuntimeException { 4 | 5 | public TemporaryFolderException(String message, Exception cause) { 6 | super(message, cause); 7 | } 8 | } -------------------------------------------------------------------------------- /src/main/java/io/github/glytching/junit/extension/folder/TemporaryFolderExtension.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.github.glytching.junit.extension.folder; 18 | 19 | import org.junit.jupiter.api.extension.ExtensionContext; 20 | import org.junit.jupiter.api.extension.ExtensionContext.Namespace; 21 | import org.junit.jupiter.api.extension.ParameterContext; 22 | import org.junit.jupiter.api.extension.ParameterResolutionException; 23 | import org.junit.jupiter.api.extension.ParameterResolver; 24 | 25 | /** 26 | * The temporary folder extension provides a test with access to temporary files and directories. 27 | * The temporary folder extension provides a {@link TemporaryFolder} which you can use to create a 28 | * temporary file or directory for use by your test. The {@link TemporaryFolder} can be injected 29 | * into your test or test case with any of the following approaches: 30 | * 31 | *

    32 | *
  • Instance variable injection into a {@code @BeforeEach} method. The {@link TemporaryFolder} 33 | * will be destroyed during {@code @AfterEach} and no exception will be thrown in cases where 34 | * the deletion fails. For example: 35 | *
     36 |  *  private TemporaryFolder temporaryFolder;
     37 |  *
     38 |  *  @BeforeEach
     39 |  *  public void setUp(TemporaryFolder temporaryFolder) {
     40 |  *      this.temporaryFolder = temporaryFolder
     41 |  *      // ...
     42 |  *  }
     43 |  * 
    44 | *
  • Parameter injection into a {@code @Test} method. The {@link TemporaryFolder} will be 45 | * destroyed during {@code @AfterEach} and no exception will be thrown in cases where the 46 | * deletion fails. For example: 47 | *
     48 |  *  @Test
     49 |  *  public void testUsingTemporaryFolder(TemporaryFolder temporaryFolder) {
     50 |  *      // ...
     51 |  *  }
     52 |  * 
    53 | *
  • Class variable injection using a {@code @BeforeAll} method. Note: in this case all 54 | * tests in the test case will share the same instance of the {@code TemporaryFolder}. The 55 | * {@link TemporaryFolder} will be destroyed after any {@code @AfterAll} method completes and 56 | * no exception will be thrown in cases where the deletion fails. For example: 57 | *
     58 |  *  private static TemporaryFolder TEMPORARY_FOLDER;
     59 |  *
     60 |  *  @BeforeAll
     61 |  *  public static void setUp(TemporaryFolder givenTemporaryFolder) {
     62 |  *      TEMPORARY_FOLDER = givenTemporaryFolder
     63 |  *      // ...
     64 |  *  }
     65 |  * 
    66 | *
67 | * 68 | *

Usage examples: 69 | * 70 | *

Injecting a {@code TemporaryFolder} in a {@code @BeforeEach} method: 71 | * 72 | *

 73 |  * @ExtendWith(TemporaryFolderExtension.class)
 74 |  * public class MyTest {
 75 |  *
 76 |  *     private TemporaryFolder temporaryFolder;
 77 |  *
 78 |  *     @BeforeEach
 79 |  *     public void setUp(TemporaryFolder temporaryFolder) {
 80 |  *         this.temporaryFolder = temporaryFolder
 81 |  *         // ...
 82 |  *     }
 83 |  *
 84 |  *     @Test
 85 |  *     public void testUsingTemporaryFile() {
 86 |  *         File file = temporaryFolder.createFile("foo.txt");
 87 |  *         // ...
 88 |  *     }
 89 |  *
 90 |  *     @Test
 91 |  *     public void testUsingTemporaryDirectory() {
 92 |  *         // use the temporary folder itself
 93 |  *         File root = temporaryFolder.getRoot();
 94 |  *
 95 |  *         // create a sub directory within the temporary folder
 96 |  *         File file = temporaryFolder.createDirectory("foo");
 97 |  *         // ...
 98 |  *     }
 99 |  * }
100 |  * 
101 | * 102 | *

Injecting a {@code TemporaryFolder} in a {@code @Test} method: 103 | * 104 | *

105 |  * public class MyTest {
106 |  *
107 |  *     @Test
108 |  *     @ExtendWith(TemporaryFolderExtension.class)
109 |  *     public void testUsingTemporaryFile(TemporaryFolder temporaryFolder) {
110 |  *         File file = temporaryFolder.createFile("foo.txt");
111 |  *         // ...
112 |  *     }
113 |  *
114 |  *     @Test
115 |  *     @ExtendWith(TemporaryFolderExtension.class)
116 |  *     public void testUsingTemporaryDirectory(TemporaryFolder temporaryFolder) {
117 |  *         // use the temporary folder itself
118 |  *         File root = temporaryFolder.getRoot();
119 |  *
120 |  *         // create a sub directory within the temporary folder
121 |  *         File file = temporaryFolder.createDirectory("foo");
122 |  *         // ...
123 |  *     }
124 |  * }
125 |  * 
126 | * 127 | * @see JUnit 4 128 | * TemporaryFolder Rule 129 | * @since 1.0.0 130 | */ 131 | public class TemporaryFolderExtension implements ParameterResolver { 132 | 133 | private static final Namespace NAMESPACE = Namespace.create(TemporaryFolderExtension.class); 134 | 135 | /** 136 | * Does this extension support injection for parameters of the type described by the given {@code 137 | * parameterContext}? 138 | * 139 | * @param parameterContext the context for the parameter for which an argument should be resolved 140 | * @param extensionContext the context in which the current test or container is being 141 | * executed 142 | * @return true if the given {@code parameterContext} describes a parameter of type: {@link 143 | * TemporaryFolder}, false otherwise 144 | * @throws ParameterResolutionException 145 | */ 146 | @Override 147 | public boolean supportsParameter( 148 | ParameterContext parameterContext, ExtensionContext extensionContext) 149 | throws ParameterResolutionException { 150 | return appliesTo(parameterContext.getParameter().getType()); 151 | } 152 | 153 | /** 154 | * Provides a value for any parameter context which has passed the {@link 155 | * #supportsParameter(ParameterContext, ExtensionContext)} gate. 156 | * 157 | * @param parameterContext the context for the parameter for which an argument should be resolved 158 | * @param extensionContext the context in which the current test or container is being 159 | * executed 160 | * @return a new {@link TemporaryFolder} 161 | * @throws ParameterResolutionException 162 | */ 163 | @Override 164 | public Object resolveParameter( 165 | ParameterContext parameterContext, ExtensionContext extensionContext) 166 | throws ParameterResolutionException { 167 | return extensionContext 168 | .getStore(NAMESPACE) 169 | .getOrComputeIfAbsent( 170 | parameterContext, key -> new TemporaryFolder(), TemporaryFolder.class); 171 | } 172 | 173 | private boolean appliesTo(Class clazz) { 174 | return clazz == TemporaryFolder.class; 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/main/java/io/github/glytching/junit/extension/random/Random.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.github.glytching.junit.extension.random; 18 | 19 | import java.lang.annotation.*; 20 | 21 | /** 22 | * Allows the caller to customise the random generation of a given type. 23 | * 24 | *

Usage example: 25 | * 26 | *

27 |  *  // create a random instance of String
28 |  *  @Random String anyString;
29 |  *
30 |  *  // create a random, fully populated instance of MyDomainObject
31 |  *  @Random private DomainObject fullyPopulatedDomainObject;
32 |  *
33 |  *  // create a random, partially populated instance of MyDomainObject, ignoring these fields: "wotsits", "id", "nestedDomainObject.address"
34 |  *  @Random(excludes = {"wotsits", "id", "nestedDomainObject.address"}) MyDomainObject partiallyPopulatedDomainObject;
35 |  *
36 |  *  // create a List containing the default size of randomly generated instances of String
37 |  *  @Random(type = String.class) List anyStrings;
38 |  *
39 |  *  // create a Stream containing two randomly generated instances of MyDomainObject
40 |  *  @Random(size = 2, type = MyDomainObject.class) Stream anyStrings;
41 |  * 
42 | */ 43 | @Retention(RetentionPolicy.RUNTIME) 44 | @Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE}) 45 | @Documented 46 | public @interface Random { 47 | 48 | /** 49 | * When generating a random type you may want to exclude some properties 50 | * 51 | * @return an array of property names to be excluded when generating a random instance of a given 52 | * type 53 | */ 54 | String[] excludes() default "[]"; 55 | 56 | /** 57 | * When generating a collection of random type you may want to limit its size. 58 | * 59 | * @return the desired size of any collections within the randomly generated type 60 | */ 61 | int size() default 10; 62 | 63 | /** 64 | * When generating a collection of random type you'll want to tell the generator what that type 65 | * is. 66 | * 67 | * @return the type of a randomly generated generic collection 68 | */ 69 | Class type() default Object.class; 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/io/github/glytching/junit/extension/random/RandomBeansExtension.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.github.glytching.junit.extension.random; 18 | 19 | import io.github.benas.randombeans.EnhancedRandomBuilder; 20 | import io.github.benas.randombeans.api.EnhancedRandom; 21 | import org.junit.jupiter.api.extension.*; 22 | 23 | import java.lang.reflect.Field; 24 | import java.lang.reflect.Modifier; 25 | import java.util.Collection; 26 | import java.util.List; 27 | import java.util.Set; 28 | import java.util.stream.Collectors; 29 | import java.util.stream.Stream; 30 | 31 | import static java.nio.charset.Charset.forName; 32 | import static org.junit.platform.commons.support.AnnotationSupport.isAnnotated; 33 | 34 | /** 35 | * The random beans extension provides a test with randomly generated objects, including: 36 | * 37 | *
    38 | *
  • JDK types 39 | *
  • Custom types 40 | *
  • Collections 41 | *
  • Generic collections 42 | *
  • Partial population 43 | *
44 | * 45 | *

Usage examples: 46 | * 47 | *

Injecting random values as fields: 48 | * 49 | *

 50 |  * @ExtendWith(RandomBeansExtension.class)
 51 |  * public class MyTest {
 52 |  *
 53 |  *     @Random
 54 |  *     private String anyString;
 55 |  *
 56 |  *     @Random(excluded = {"name", "value"})
 57 |  *     private List anyPartiallyPopulatedDomainObject;
 58 |  *
 59 |  *     @Random(type = DomainObject.class)
 60 |  *     private List anyDomainObjects;
 61 |  *
 62 |  *     @Test
 63 |  *     public void testUsingRandomString() {
 64 |  *         // use the injected anyString
 65 |  *         // ...
 66 |  *     }
 67 |  *
 68 |  *     @Test
 69 |  *     public void testUsingRandomDomainObjects() {
 70 |  *         // use the injected anyDomainObjects
 71 |  *         // the anyDomainObjects will contain _N_ fully populated random instances of DomainObject
 72 |  *         // ...
 73 |  *     }
 74 |  *
 75 |  *     @Test
 76 |  *     public void testUsingPartiallyPopulatedDomainObject() {
 77 |  *         // use the injected anyPartiallyPopulatedDomainObject
 78 |  *         // this object's "name" and "value" members will not be populated since this has been declared with
 79 |  *         //     excluded = {"name", "value"}
 80 |  *         // ...
 81 |  *     }
 82 |  * }
 83 |  * 
84 | * 85 | *

Injecting random values as parameters: 86 | * 87 | *

 88 |  * @ExtendWith(RandomBeansExtension.class)
 89 |  * public class MyTest {
 90 |  *
 91 |  *     @Test
 92 |  *     @ExtendWith(RandomBeansExtension.class)
 93 |  *     public void testUsingRandomString(@Random String anyString) {
 94 |  *         // use the provided anyString
 95 |  *         // ...
 96 |  *     }
 97 |  *
 98 |  *     @Test
 99 |  *     @ExtendWith(RandomBeansExtension.class)
100 |  *     public void testUsingRandomDomainObjects(@Random(type = DomainObject.class) List anyDomainObjects) {
101 |  *         // use the injected anyDomainObjects
102 |  *         // the anyDomainObjects will contain _N_ fully populated random instances of DomainObject
103 |  *         // ...
104 |  *     }
105 |  *
106 |  *     @Test
107 |  *     @ExtendWith(RandomBeansExtension.class)
108 |  *     public void testUsingPartiallyPopulatedDomainObject(@Random(excluded = {"name", "value"}) List anyPartiallyPopulatedDomainObject) {
109 |  *         // use the injected anyPartiallyPopulatedDomainObject
110 |  *         // this object's "name" and "value" members will not be populated since this has been declared with
111 |  *         //     excluded = {"name", "value"}
112 |  *         // ...
113 |  *     }
114 |  * }
115 |  * 
116 | * 117 | * @see Random Beans 118 | * @since 1.0.0 119 | */ 120 | public class RandomBeansExtension implements TestInstancePostProcessor, ParameterResolver { 121 | 122 | private final EnhancedRandom random; 123 | 124 | /** 125 | * Create the extension with a default {@link EnhancedRandom}. 126 | * 127 | * @see Enhanced 128 | * Random Configuration Parameters 129 | */ 130 | public RandomBeansExtension() { 131 | this(EnhancedRandomBuilder.aNewEnhancedRandomBuilder() 132 | // maximum number of instances of a given type, above this number requests will start to 133 | // reuse 134 | // previously generated instances 135 | .objectPoolSize(10) 136 | 137 | // how deep should we go when randomising an object graph? 138 | .randomizationDepth(5) 139 | 140 | // the charset used for all String and Character values 141 | .charset(forName("UTF-8")) 142 | 143 | // min, max bounds for the generated string length 144 | .stringLengthRange(5, 50) 145 | 146 | // min, max bounds for the generated collections size 147 | .collectionSizeRange(1, 10) 148 | 149 | // if a random values is declared as an abstract or interface type then the classpath 150 | // will be scanned 151 | // for a concrete type of that abstract or interface type 152 | .scanClasspathForConcreteTypes(true) 153 | 154 | // do not override any values which are already initialised in the target type 155 | .overrideDefaultInitialization(false) 156 | .build()); 157 | } 158 | 159 | /** 160 | * Create the extension with the given {@link EnhancedRandom}. This is used, instead of the zero-arg alternative, when 161 | * the caller wants to override the default 'randomizer' configuration. This constructor will be called by using the 162 | * {@code RegisterExtension} annotation. 163 | * 164 | * @param enhancedRandom 165 | * @since 2.5.0 166 | */ 167 | public RandomBeansExtension(EnhancedRandom enhancedRandom) { 168 | this.random = enhancedRandom; 169 | 170 | } 171 | 172 | /** 173 | * Does this extension support injection for parameters of the type described by the given {@code 174 | * parameterContext}? 175 | * 176 | * @param parameterContext the context for the parameter for which an argument should be resolved 177 | * @param extensionContext the extension context for the {@code Executable} about to be invoked 178 | * @return true if the given {@code parameterContext} is annotated with {@link Random}, false 179 | * otherwise 180 | * @throws ParameterResolutionException 181 | */ 182 | @Override 183 | public boolean supportsParameter( 184 | ParameterContext parameterContext, ExtensionContext extensionContext) 185 | throws ParameterResolutionException { 186 | return parameterContext.getParameter().getAnnotation(Random.class) != null; 187 | } 188 | 189 | /** 190 | * Provides a value for any parameter context which has passed the {@link 191 | * #supportsParameter(ParameterContext, ExtensionContext)} gate. 192 | * 193 | * @param parameterContext the context for the parameter for which an argument should be resolved 194 | * @param extensionContext the context in which the current test or container is being 195 | * executed 196 | * @return a randomly generated object 197 | * @throws ParameterResolutionException 198 | */ 199 | @Override 200 | public Object resolveParameter( 201 | ParameterContext parameterContext, ExtensionContext extensionContext) 202 | throws ParameterResolutionException { 203 | return resolve( 204 | parameterContext.getParameter().getType(), 205 | parameterContext.getParameter().getAnnotation(Random.class)); 206 | } 207 | 208 | /** 209 | * Inject random values into any fields which are annotated with {@link Random}. 210 | * This method doesn't populate static fields if they have values. 211 | * 212 | * @param testInstance the instance to post-process 213 | * @param extensionContext the current extension context 214 | * @throws Exception 215 | */ 216 | @Override 217 | public void postProcessTestInstance(Object testInstance, ExtensionContext extensionContext) 218 | throws Exception { 219 | for (Field field : testInstance.getClass().getDeclaredFields()) { 220 | if (isAnnotated(field, Random.class)) { 221 | Random annotation = field.getAnnotation(Random.class); 222 | Object randomObject = resolve(field.getType(), annotation); 223 | 224 | field.setAccessible(true); 225 | if (!Modifier.isStatic(field.getModifiers()) || field.get(testInstance) == null) { 226 | field.set(testInstance, randomObject); 227 | } 228 | } 229 | } 230 | } 231 | 232 | /** 233 | * Maps the 'random requirements' expressed by the given {@code annotation} to invocations on 234 | * {@link #random}. 235 | * 236 | * @param targetType the type to be provided 237 | * @param annotation an instance of {@link Random} which describes how the user wishes to 238 | * configure the 'random generation' 239 | * @return a randomly generated instance of {@code targetType} 240 | */ 241 | private Object resolve(Class targetType, Random annotation) { 242 | if (targetType.isAssignableFrom(List.class) || targetType.isAssignableFrom(Collection.class)) { 243 | return random 244 | .objects(annotation.type(), annotation.size(), annotation.excludes()) 245 | .collect(Collectors.toList()); 246 | } else if (targetType.isAssignableFrom(Set.class)) { 247 | return random 248 | .objects(annotation.type(), annotation.size(), annotation.excludes()) 249 | .collect(Collectors.toSet()); 250 | } else if (targetType.isAssignableFrom(Stream.class)) { 251 | return random.objects(annotation.type(), annotation.size(), annotation.excludes()); 252 | } else { 253 | return random.nextObject(targetType, annotation.excludes()); 254 | } 255 | } 256 | 257 | } 258 | -------------------------------------------------------------------------------- /src/main/java/io/github/glytching/junit/extension/system/RestoreContext.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.github.glytching.junit.extension.system; 18 | 19 | import java.util.HashMap; 20 | import java.util.HashSet; 21 | import java.util.Map; 22 | import java.util.Set; 23 | 24 | /** 25 | * A context object which encapsulates what the system property extension did. This allows us to 26 | * reverse any changes made by the extension after test execution completes. For example: 27 | * 28 | *
    29 | *
  • If a new system property was added then we remove it 30 | *
  • If an existing system property was overwritten then we reinstate its original value 31 | *
32 | */ 33 | final class RestoreContext { 34 | private final Set propertyNames; 35 | private final Map restoreProperties; 36 | 37 | /** 38 | * Created using the {@link Builder}. 39 | * 40 | * @param propertyNames 41 | * @param restoreProperties 42 | */ 43 | private RestoreContext(Set propertyNames, Map restoreProperties) { 44 | this.propertyNames = new HashSet<>(propertyNames); 45 | this.restoreProperties = new HashMap<>(restoreProperties); 46 | } 47 | 48 | public static Builder createBuilder() { 49 | return new Builder(); 50 | } 51 | 52 | /** 53 | * Reverse the system property 'sets' performed on behalf of this restore context. 54 | * 55 | *

For each entry in {@link #propertyNames}, if {@link #restoreProperties} contains an entry 56 | * then reset the system property with the value from {@link #restoreProperties} otherwise just 57 | * remove the system property for that property name. 58 | */ 59 | public void restore() { 60 | for (String propertyName : propertyNames) { 61 | if (restoreProperties.containsKey(propertyName)) { 62 | // reinstate the original value 63 | System.setProperty(propertyName, restoreProperties.get(propertyName)); 64 | } else { 65 | // remove the (previously unset) property 66 | System.clearProperty(propertyName); 67 | } 68 | } 69 | } 70 | 71 | /** 72 | * Simple builder implementation allowing a {@link RestoreContext} to be built up as we walk 73 | * through system property configuration. 74 | */ 75 | final static class Builder { 76 | private final Set properties; 77 | private final Map restoreProperties; 78 | 79 | private Builder() { 80 | properties = new HashSet<>(); 81 | restoreProperties = new HashMap<>(); 82 | } 83 | 84 | void addPropertyName(String propertyName) { 85 | properties.add(propertyName); 86 | } 87 | 88 | void addRestoreProperty(String propertyName, String propertyValue) { 89 | restoreProperties.put(propertyName, propertyValue); 90 | } 91 | 92 | RestoreContext build() { 93 | return new RestoreContext(properties, restoreProperties); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/io/github/glytching/junit/extension/system/SystemProperties.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.github.glytching.junit.extension.system; 18 | 19 | import org.junit.jupiter.api.extension.ExtendWith; 20 | 21 | import java.lang.annotation.*; 22 | 23 | /** 24 | * A repeatable annotation for {@link SystemProperty}. This annotation can be used at class level 25 | * and at method level. 26 | * 27 | *

Usage example: 28 | * 29 | *

30 |  *  // set the system properties nameA:valueA and nameB:valueB
31 |  *  @SystemProperty(name = "nameA", value = "valueA")
32 |  *  @SystemProperty(name = "nameB", value = "valueB")
33 |  * 
34 | */ 35 | @Retention(RetentionPolicy.RUNTIME) 36 | @Target({ElementType.TYPE, ElementType.METHOD, ElementType.ANNOTATION_TYPE}) 37 | @Documented 38 | @ExtendWith(SystemPropertyExtension.class) 39 | public @interface SystemProperties { 40 | 41 | SystemProperty[] value(); 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/io/github/glytching/junit/extension/system/SystemProperty.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.github.glytching.junit.extension.system; 18 | 19 | import org.junit.jupiter.api.extension.ExtendWith; 20 | 21 | import java.lang.annotation.*; 22 | 23 | 24 | /** 25 | * Declares a system property to be set before a test. This annotation can be used at class level 26 | * and at method level. 27 | * 28 | *

Usage example: 29 | * 30 | *

31 |  *  // set the system properties nameA:valueA
32 |  *  @SystemProperty(name = "nameA", value = "valueA")
33 |  * 
34 | */ 35 | @Retention(RetentionPolicy.RUNTIME) 36 | @Target({ElementType.TYPE, ElementType.METHOD, ElementType.ANNOTATION_TYPE}) 37 | @Documented 38 | @Repeatable(value = SystemProperties.class) 39 | @ExtendWith(SystemPropertyExtension.class) 40 | public @interface SystemProperty { 41 | 42 | String name(); 43 | 44 | String value(); 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/io/github/glytching/junit/extension/system/SystemPropertyExtension.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.github.glytching.junit.extension.system; 18 | 19 | import org.junit.jupiter.api.extension.*; 20 | 21 | import java.lang.reflect.AnnotatedElement; 22 | import java.util.ArrayList; 23 | import java.util.Arrays; 24 | import java.util.List; 25 | 26 | import static io.github.glytching.junit.extension.util.ExtensionUtil.getStore; 27 | import static org.junit.platform.commons.util.AnnotationUtils.isAnnotated; 28 | 29 | /** 30 | * The system property extension sets system properties before test execution and unsets them on 31 | * completion. More specifically: 32 | * 33 | *
    34 | *
  • If a new system property was added then it is removed after test execution completes 35 | *
  • If an existing system property was overwritten then its original value is reinstated after 36 | * test execution completes 37 | *
38 | * 39 | *

System properties can be injected into your test or test case with either of the following 40 | * approaches: 41 | * 42 | *

    43 | *
  • Class level annotation. Note: you cannot annotate a base test case and expect the extension 44 | * to be engaged for its children. So, if you want a system property to be available for all 45 | * test methods in a Class {@code X} which extends {@code Y} then you must add the annotation 46 | * to Class {@code X} not to Class {@code Y}. For example: 47 | *
     48 |  *  @SystemProperty(name = "nameA", value = "valueA")
     49 |  *  public class MyTest {
     50 |  *      // ...
     51 |  *  }
     52 |  * 
    53 | *
  • Parameter injection into a {@code @Test} method. For example: 54 | *
     55 |  *  @Test
     56 |  *  @SystemProperty(name = "nameA", value = "valueA")
     57 |  *  public void testUsingSystemProperty() {
     58 |  *      // ...
     59 |  *  }
     60 |  * 
    61 | *
62 | * 63 | *

The {@link SystemProperty} annotation is repeatable. 64 | * 65 | *

Usage examples: 66 | * 67 | *

Declaring system properties at class level: 68 | * 69 | *

 70 |  *  @SystemProperty(name = "nameA", value = "valueA")
 71 |  *  @SystemProperty(name = "nameB", value = "valueB")
 72 |  * public class MyTest {
 73 |  *
 74 |  *     @Test
 75 |  *     public void test() {
 76 |  *         // the system properties nameA:valueA, nameB:valueB have been set
 77 |  *         // ...
 78 |  *     }
 79 |  * }
 80 |  * 
81 | * 82 | *

Declaring system properties at method level: 83 | * 84 | *

 85 |  * public class MyTest {
 86 |  *
 87 |  *     @Test
 88 |  *     @SystemProperty(name = "nameA", value = "valueA")
 89 |  *     @SystemProperty(name = "nameB", value = "valueB")
 90 |  *     public void testUsingSystemProperties(TemporaryFolder temporaryFolder) {
 91 |  *         // the system properties nameA:valueA, nameB:valueB have been set
 92 |  *         // ...
 93 |  *     }
 94 |  *
 95 |  *     @Test
 96 |  *     @SystemProperty(name = "nameC", value = "valueC")
 97 |  *     public void testUsingSystemProperty(TemporaryFolder temporaryFolder) {
 98 |  *         // the system property nameC:valueC has been set
 99 |  *         // ...
100 |  *     }
101 |  *
102 |  *     @Test
103 |  *     public void testWithoutSystemProperties() {
104 |  *         // the system properties nameA:valueA, nameB:valueB, nameC:valueC have *not* been set
105 |  *         // ...
106 |  *     }
107 |  * }
108 |  * 
109 | * 110 | * @since 1.0.0 111 | */ 112 | public class SystemPropertyExtension 113 | implements AfterEachCallback, BeforeEachCallback, BeforeAllCallback, AfterAllCallback { 114 | 115 | private static final String KEY = "restoreContext"; 116 | 117 | /** 118 | * If the current test class has a system property annotation(s) then create a {@link 119 | * RestoreContext} representing the annotation(s). This causes the requested system properties to 120 | * be set and retains a copy of pre-set values for reinstatement after test execution. 121 | * 122 | * @param extensionContext the context in which the current test or container is being 123 | * executed 124 | * @throws Exception 125 | */ 126 | @Override 127 | public void beforeAll(ExtensionContext extensionContext) throws Exception { 128 | List systemProperties = 129 | getSystemProperties(extensionContext.getRequiredTestClass()); 130 | if (!systemProperties.isEmpty()) { 131 | RestoreContext.Builder builder = RestoreContext.createBuilder(); 132 | for (SystemProperty systemProperty : systemProperties) { 133 | builder.addPropertyName(systemProperty.name()); 134 | if (System.getProperty(systemProperty.name()) != null) { 135 | builder.addRestoreProperty( 136 | systemProperty.name(), System.getProperty(systemProperty.name())); 137 | } 138 | 139 | set(systemProperty); 140 | } 141 | writeRestoreContext(extensionContext, builder.build()); 142 | } 143 | } 144 | 145 | /** 146 | * If a {@link RestoreContext} exists for the given {@code extensionContext} then restore it i.e. 147 | * unset any system properties which were set in {@link #beforeAll(ExtensionContext)} for this 148 | * {@code extensionContext} and reinstate original value, if applicable. 149 | * 150 | * @param extensionContext the context in which the current test or container is being 151 | * executed 152 | * @throws Exception 153 | */ 154 | @Override 155 | public void afterAll(ExtensionContext extensionContext) throws Exception { 156 | RestoreContext restoreContext = readRestoreContext(extensionContext); 157 | if (restoreContext != null) { 158 | restoreContext.restore(); 159 | } 160 | } 161 | 162 | /** 163 | * If the current test method has a system property annotation(s) then create a {@link 164 | * RestoreContext} representing the annotation(s). This causes the requested system properties to 165 | * be set and retains a copy of pre-set values for reinstatement after test execution. 166 | * 167 | * @param extensionContext the context in which the current test or container is being 168 | * executed 169 | * @throws Exception 170 | */ 171 | @Override 172 | public void beforeEach(ExtensionContext extensionContext) throws Exception { 173 | List systemProperties = 174 | getSystemProperties(extensionContext.getRequiredTestMethod()); 175 | if (!systemProperties.isEmpty()) { 176 | RestoreContext.Builder builder = RestoreContext.createBuilder(); 177 | for (SystemProperty systemProperty : systemProperties) { 178 | builder.addPropertyName(systemProperty.name()); 179 | if (System.getProperty(systemProperty.name()) != null) { 180 | builder.addRestoreProperty( 181 | systemProperty.name(), System.getProperty(systemProperty.name())); 182 | } 183 | 184 | set(systemProperty); 185 | } 186 | writeRestoreContext(extensionContext, builder.build()); 187 | } 188 | } 189 | 190 | /** 191 | * If a {@link RestoreContext} exists for the given {@code extensionContext} then restore it i.e. 192 | * unset any system properties which were set in {@link #beforeEach(ExtensionContext)} for this 193 | * {@code extensionContext} and reinstate original value, if applicable. 194 | * 195 | * @param extensionContext the context in which the current test or container is being 196 | * executed 197 | * @throws Exception 198 | */ 199 | @Override 200 | public void afterEach(ExtensionContext extensionContext) throws Exception { 201 | RestoreContext restoreContext = readRestoreContext(extensionContext); 202 | if (restoreContext != null) { 203 | restoreContext.restore(); 204 | } 205 | } 206 | 207 | /** 208 | * Get a collection of {@link SystemProperty} for the given {@code annotatedElement}. If the given 209 | * {@code annotatedElement} has no such annotations then an empty list is returned, if the given 210 | * {@code annotatedElement} is annotated with {@link SystemProperty} then a list with one element 211 | * is returned, if the given {@code annotatedElement} is annotated with {@link SystemProperties} 212 | * then a list with one element for each of the repeated {@link SystemProperty} values is 213 | * returned. 214 | * 215 | *

This is essentially a shortcut for logic such as: 'does this element have the {@link 216 | * SystemProperty} annotation, if not does it have the {@link SystemProperties}' followed by 217 | * gathering these annotation values. 218 | * 219 | * @param annotatedElement either a test class or a test method which may be annotated with a 220 | * system property annotation 221 | * @return 0..* {@link SystemProperty} elements 222 | */ 223 | private List getSystemProperties(AnnotatedElement annotatedElement) { 224 | List systemProperties = new ArrayList<>(); 225 | if (isAnnotated(annotatedElement, SystemProperties.class)) { 226 | // gather the repeating system property values 227 | systemProperties.addAll( 228 | Arrays.asList(annotatedElement.getAnnotation(SystemProperties.class).value())); 229 | } 230 | if (isAnnotated(annotatedElement, SystemProperty.class)) { 231 | // add the single system property value 232 | systemProperties.add(annotatedElement.getAnnotation(SystemProperty.class)); 233 | } 234 | return systemProperties; 235 | } 236 | 237 | private void set(SystemProperty systemProperty) { 238 | System.setProperty(systemProperty.name(), systemProperty.value()); 239 | } 240 | 241 | private void writeRestoreContext( 242 | ExtensionContext extensionContext, RestoreContext restoreContext) { 243 | getStore(extensionContext, this.getClass()).getOrComputeIfAbsent(KEY, key -> restoreContext); 244 | } 245 | 246 | private RestoreContext readRestoreContext(ExtensionContext extensionContext) { 247 | return getStore(extensionContext, this.getClass()).get(KEY, RestoreContext.class); 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /src/main/java/io/github/glytching/junit/extension/testname/TestName.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.github.glytching.junit.extension.testname; 18 | 19 | import java.lang.annotation.*; 20 | 21 | /** 22 | * Declares a instance variable to be populated with the name of the currently executing test. 23 | * 24 | *

Usage example: 25 | * 26 | *

27 |  *  @TestName
28 |  *  private String testName;
29 |  * 
30 | */ 31 | @Retention(RetentionPolicy.RUNTIME) 32 | @Target({ElementType.FIELD}) 33 | @Documented 34 | public @interface TestName {} 35 | -------------------------------------------------------------------------------- /src/main/java/io/github/glytching/junit/extension/testname/TestNameExtension.java: -------------------------------------------------------------------------------- 1 | package io.github.glytching.junit.extension.testname; 2 | 3 | import org.junit.jupiter.api.extension.AfterTestExecutionCallback; 4 | import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; 5 | import org.junit.jupiter.api.extension.ExtensionContext; 6 | 7 | import java.lang.reflect.Field; 8 | import java.util.Optional; 9 | 10 | import static org.junit.platform.commons.support.AnnotationSupport.isAnnotated; 11 | 12 | /** 13 | * The test name extension makes the current test name available inside each test method. 14 | * 15 | *

Usage example: 16 | * 17 | *

Injecting random values as fields: 18 | * 19 | *

20 |  * @ExtendWith(TestNameExtension.class)
21 |  * public class MyTest {
22 |  *
23 |  *     @TestName
24 |  *     private String testName;
25 |  *
26 |  *     @Test
27 |  *     public void testUsingRandomString() {
28 |  *         // use the populated testName
29 |  *         // ...
30 |  *     }
31 |  * }
32 |  * 
33 | * 34 | * @since 1.1.0 35 | */ 36 | public class TestNameExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback { 37 | 38 | @Override 39 | public void beforeTestExecution(ExtensionContext extensionContext) throws Exception { 40 | setTestNameFieldValue( 41 | getTestNameField(extensionContext), 42 | extensionContext.getRequiredTestInstance(), 43 | extensionContext.getRequiredTestMethod().getName()); 44 | } 45 | 46 | @Override 47 | public void afterTestExecution(ExtensionContext extensionContext) throws Exception { 48 | setTestNameFieldValue( 49 | getTestNameField(extensionContext), extensionContext.getRequiredTestInstance(), null); 50 | } 51 | 52 | private Optional getTestNameField(ExtensionContext extensionContext) { 53 | for (Field field : extensionContext.getRequiredTestClass().getDeclaredFields()) { 54 | if (isAnnotated(field, TestName.class)) { 55 | return Optional.of(field); 56 | } 57 | } 58 | return Optional.empty(); 59 | } 60 | 61 | @SuppressWarnings("OptionalUsedAsFieldOrParameterType") 62 | private void setTestNameFieldValue( 63 | Optional testNameField, Object testInstance, String value) throws IllegalAccessException { 64 | if (testNameField.isPresent()) { 65 | testNameField.get().setAccessible(true); 66 | testNameField.get().set(testInstance, value); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/io/github/glytching/junit/extension/util/ExtensionUtil.java: -------------------------------------------------------------------------------- 1 | package io.github.glytching.junit.extension.util; 2 | 3 | import org.junit.jupiter.api.extension.ExtensionContext; 4 | 5 | public final class ExtensionUtil { 6 | 7 | // this is a utility class - hide the public ctor 8 | private ExtensionUtil() {} 9 | 10 | /** 11 | * Creates a {@link ExtensionContext.Store} for a given {@code extensionContext}. A {@link 12 | * ExtensionContext.Store} is bound to an {@link ExtensionContext} so different test invocations 13 | * do not share the same store. For example a test invocation on {@code ClassA.testMethodA} will 14 | * have a different {@link ExtensionContext.Store} instance to that associated with a test 15 | * invocation on {@code ClassA.testMethodB} or test invocation on {@code ClassC.testMethodC}. 16 | * 17 | * @param extensionContext the context in which the current test or container is being 18 | * executed 19 | * @return a {@link ExtensionContext.Store} for the given {@code extensionContext} 20 | */ 21 | public static ExtensionContext.Store getStore(ExtensionContext extensionContext, Class clazz) { 22 | return extensionContext.getStore(namespace(extensionContext, clazz)); 23 | } 24 | 25 | /** 26 | * Creates a {@link ExtensionContext.Namespace} in which extension state is stored on creation for 27 | * post execution destruction. Storing data in a custom namespace prevents accidental cross 28 | * pollination of data between extensions and between different invocations within the lifecycle 29 | * of a single extension. 30 | * 31 | * @param extensionContext the context in which the current test or container is being 32 | * executed 33 | * @return a {@link ExtensionContext.Namespace} describing the scope for an extension 34 | */ 35 | private static ExtensionContext.Namespace namespace( 36 | ExtensionContext extensionContext, Class clazz) { 37 | return ExtensionContext.Namespace.create(clazz, extensionContext); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/io/github/glytching/junit/extension/watcher/WatcherExtension.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.github.glytching.junit.extension.watcher; 18 | 19 | import org.junit.jupiter.api.extension.AfterTestExecutionCallback; 20 | import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; 21 | import org.junit.jupiter.api.extension.ExtensionContext; 22 | import org.junit.jupiter.api.extension.ExtensionContext.Store; 23 | 24 | import java.lang.reflect.Method; 25 | import java.util.logging.Logger; 26 | 27 | import static io.github.glytching.junit.extension.util.ExtensionUtil.getStore; 28 | 29 | /** 30 | * The watcher extension logs test execution flow including: 31 | * 32 | *
    33 | *
  • Entry 34 | *
  • Exit 35 | *
  • Elapsed time in ms 36 | *
37 | * 38 | *

It produces output like so: 39 | * 40 | *

 41 |  * INFO: Starting test: [aTest]
 42 |  * INFO: Completed test [aTest] in 21 ms.
 43 |  * 
44 | * 45 | *

Usage example: 46 | * 47 | *

 48 |  * @ExtendWith(WatcherExtension.class)
 49 |  * public class MyTest {
 50 |  *
 51 |  *  @Test
 52 |  *  public void aTest() {
 53 |  *      // ...
 54 |  *  }
 55 |  * }
 56 |  * 
57 | * 58 | * @see JUnit 4 60 | * TestWatchman/TestWatcher Rules 61 | * @since 1.0.0 62 | */ 63 | public class WatcherExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback { 64 | private final Logger logger; 65 | 66 | WatcherExtension() { 67 | this(Logger.getLogger(WatcherExtension.class.getName())); 68 | } 69 | 70 | // erm, facilitates testing 71 | WatcherExtension(Logger logger) { 72 | this.logger = logger; 73 | } 74 | 75 | /** 76 | * Log test method entry and store its start time in the {@link Store} for use in {@link 77 | * #afterTestExecution(ExtensionContext)}. 78 | * 79 | * @param extensionContext the context in which the current test or container is being 80 | * executed 81 | * @throws Exception 82 | */ 83 | @Override 84 | public void beforeTestExecution(ExtensionContext extensionContext) throws Exception { 85 | Method testMethod = extensionContext.getRequiredTestMethod(); 86 | logger.info(String.format("Starting test [%s]", testMethod.getName())); 87 | getStore(extensionContext, this.getClass()).put(testMethod, System.currentTimeMillis()); 88 | } 89 | 90 | /** 91 | * Log test method exit, using the start time stored by {@link 92 | * #beforeTestExecution(ExtensionContext)} to calculate a duration. 93 | * 94 | * @param extensionContext the context in which the current test or container is being 95 | * executed 96 | * @throws Exception 97 | */ 98 | @Override 99 | public void afterTestExecution(ExtensionContext extensionContext) throws Exception { 100 | Method testMethod = extensionContext.getRequiredTestMethod(); 101 | long start = getStore(extensionContext, this.getClass()).remove(testMethod, long.class); 102 | long duration = System.currentTimeMillis() - start; 103 | 104 | logger.info(String.format("Completed test [%s] in %sms", testMethod.getName(), duration)); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/test/java/io/github/glytching/junit/extension/CompositeExtensionTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.github.glytching.junit.extension; 18 | 19 | import io.github.glytching.junit.extension.exception.ExpectedException; 20 | import io.github.glytching.junit.extension.folder.TemporaryFolder; 21 | import io.github.glytching.junit.extension.folder.TemporaryFolderExtension; 22 | import io.github.glytching.junit.extension.random.Random; 23 | import io.github.glytching.junit.extension.random.RandomBeansExtension; 24 | import io.github.glytching.junit.extension.system.SystemProperty; 25 | import io.github.glytching.junit.extension.watcher.WatcherExtension; 26 | import org.junit.jupiter.api.Test; 27 | import org.junit.jupiter.api.extension.ExtendWith; 28 | 29 | import java.io.File; 30 | import java.io.IOException; 31 | 32 | import static org.hamcrest.MatcherAssert.assertThat; 33 | import static org.hamcrest.Matchers.is; 34 | import static org.hamcrest.Matchers.notNullValue; 35 | 36 | @SystemProperty(name = "x", value = "y") 37 | @ExtendWith({RandomBeansExtension.class, WatcherExtension.class}) 38 | public class CompositeExtensionTest { 39 | 40 | @Random private String anyString; 41 | 42 | @Test 43 | @ExtendWith(TemporaryFolderExtension.class) 44 | @ExpectedException(type = RuntimeException.class, messageIs = "Doh!") 45 | public void canHandleTheKitchenSink(TemporaryFolder temporaryFolder, @Random Long anyLong) 46 | throws IOException { 47 | // randomness 48 | assertThat(anyString, notNullValue()); 49 | assertThat(anyLong, notNullValue()); 50 | 51 | // system property 52 | assertThat(System.getProperty("x"), is("y")); 53 | 54 | // temporary folder 55 | File file = temporaryFolder.createFile("foo.txt"); 56 | assertThat(file.exists(), is(true)); 57 | 58 | // expected exception 59 | throw new RuntimeException("Doh!"); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/test/java/io/github/glytching/junit/extension/benchmark/BenchmarkExtensionTest.java: -------------------------------------------------------------------------------- 1 | package io.github.glytching.junit.extension.benchmark; 2 | 3 | import io.github.glytching.junit.extension.util.ExecutionEvent; 4 | import io.github.glytching.junit.extension.util.RecordingExecutionListener; 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.api.extension.ExtendWith; 8 | import org.junit.jupiter.api.extension.RegisterExtension; 9 | import org.junit.jupiter.engine.JupiterTestEngine; 10 | import org.junit.platform.engine.ExecutionRequest; 11 | import org.junit.platform.engine.TestDescriptor; 12 | import org.junit.platform.engine.UniqueId; 13 | import org.junit.platform.engine.reporting.ReportEntry; 14 | import org.junit.platform.launcher.LauncherDiscoveryRequest; 15 | 16 | import java.util.List; 17 | import java.util.concurrent.TimeUnit; 18 | import java.util.stream.Collectors; 19 | 20 | import static java.lang.String.format; 21 | import static java.util.concurrent.TimeUnit.MICROSECONDS; 22 | import static java.util.concurrent.TimeUnit.MILLISECONDS; 23 | import static org.hamcrest.MatcherAssert.assertThat; 24 | import static org.hamcrest.Matchers.equalTo; 25 | import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; 26 | import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request; 27 | 28 | public class BenchmarkExtensionTest { 29 | 30 | private JupiterTestEngine engine; 31 | private RecordingExecutionListener listener; 32 | 33 | @BeforeEach 34 | public void setUp() { 35 | engine = new JupiterTestEngine(); 36 | listener = new RecordingExecutionListener(); 37 | } 38 | 39 | @Test 40 | void willPublishBenchmarkResultsWithDefaultTimeUnit() { 41 | // when executing a test case 42 | execute(DefaultTimeUnitBenchmarkTest.class); 43 | 44 | // then the benchmark report event(s) are published 45 | List publishedEvents = getReportEntries(); 46 | 47 | assertThat(publishedEvents.size(), equalTo(1)); 48 | 49 | assertThat( 50 | toReportEntryKey(publishedEvents.get(0)), 51 | equalTo(format("Elapsed time in %s for canBenchmark()", MILLISECONDS.name()))); 52 | } 53 | 54 | @Test 55 | void willPublishBenchmarkResultsWithChosenTimeUnit() { 56 | // when executing a test case 57 | execute(CustomTimeUnitBenchmarkTest.class); 58 | 59 | // then the benchmark report event(s) are published 60 | List publishedEvents = getReportEntries(); 61 | 62 | assertThat(publishedEvents.size(), equalTo(1)); 63 | 64 | assertThat( 65 | toReportEntryKey(publishedEvents.get(0)), 66 | equalTo(format("Elapsed time in %s for canBenchmark()", MICROSECONDS.name()))); 67 | } 68 | 69 | private void execute(Class clazz) { 70 | LauncherDiscoveryRequest request = getRequest(clazz); 71 | engine.execute( 72 | new ExecutionRequest( 73 | getTestDescriptor(request), listener, request.getConfigurationParameters())); 74 | } 75 | 76 | private LauncherDiscoveryRequest getRequest(Class clazz) { 77 | return request().selectors(selectClass(clazz)).build(); 78 | } 79 | 80 | private TestDescriptor getTestDescriptor(LauncherDiscoveryRequest request) { 81 | return engine.discover(request, UniqueId.forEngine(engine.getId())); 82 | } 83 | 84 | private List getReportEntries() { 85 | return listener 86 | .getEventsByType(ExecutionEvent.Type.REPORTING_ENTRY_PUBLISHED) 87 | .collect(Collectors.toList()); 88 | } 89 | 90 | private String toReportEntryKey(ExecutionEvent event) { 91 | return event.getPayload(ReportEntry.class).get().getKeyValuePairs().keySet().iterator().next() 92 | + "()"; 93 | } 94 | 95 | @ExtendWith(BenchmarkExtension.class) 96 | static class DefaultTimeUnitBenchmarkTest { 97 | 98 | @Test 99 | public void canBenchmark() throws InterruptedException { 100 | // note: the actual assertion - verifying publication of report events - is performed in the 101 | // containing class 102 | Thread.sleep(5); 103 | } 104 | } 105 | 106 | static class CustomTimeUnitBenchmarkTest { 107 | 108 | @SuppressWarnings("unused") 109 | @RegisterExtension 110 | static BenchmarkExtension benchmarkExtension = new BenchmarkExtension(TimeUnit.MICROSECONDS); 111 | 112 | @Test 113 | public void canBenchmark() throws InterruptedException { 114 | // note: the actual assertion - verifying publication of report events - is performed in the 115 | // containing class 116 | Thread.sleep(5); 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/test/java/io/github/glytching/junit/extension/exception/ExpectedExceptionExtensionMetaTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.github.glytching.junit.extension.exception; 18 | 19 | import org.junit.jupiter.api.BeforeEach; 20 | import org.junit.jupiter.api.Test; 21 | import org.junit.jupiter.api.extension.ExtensionContext; 22 | import org.mockito.Mock; 23 | import org.mockito.MockitoAnnotations; 24 | import org.opentest4j.AssertionFailedError; 25 | 26 | import java.lang.reflect.Method; 27 | import java.util.Optional; 28 | import java.util.function.Function; 29 | 30 | import static org.hamcrest.MatcherAssert.assertThat; 31 | import static org.hamcrest.Matchers.is; 32 | import static org.junit.jupiter.api.Assertions.assertThrows; 33 | import static org.junit.jupiter.api.extension.ExtensionContext.Namespace.create; 34 | import static org.junit.jupiter.api.extension.ExtensionContext.Store; 35 | import static org.mockito.ArgumentMatchers.any; 36 | import static org.mockito.Mockito.when; 37 | 38 | /** 39 | * We cannot use the normal test flow to verify the path where a thrown exception does not 40 | * match the expected exception because this path causes the exception to be rethrown which would 41 | * then cause the test to fail. So, we have to test the sad path outside the normal test 42 | * flow by playing around with the {@link ExpectedExceptionExtension} directly. 43 | */ 44 | public class ExpectedExceptionExtensionMetaTest { 45 | 46 | private final ExpectedExceptionExtension sut = new ExpectedExceptionExtension(); 47 | 48 | @Mock private ExtensionContext extensionContext; 49 | @Mock private Store store; 50 | 51 | @BeforeEach 52 | public void initMocks() { 53 | MockitoAnnotations.initMocks(this); 54 | } 55 | 56 | @Test 57 | public void willRethrowIfTheExceptionTypeDoesNotMatchTheExpectedExceptionType() throws Throwable { 58 | givenExtensionContentWithMethod("canHandleAThrowable"); 59 | 60 | RuntimeException expected = new RuntimeException(""); 61 | 62 | RuntimeException actual = 63 | assertThrows( 64 | expected.getClass(), 65 | () -> sut.handleTestExecutionException(extensionContext, expected)); 66 | assertThat(actual, is(expected)); 67 | } 68 | 69 | @Test 70 | public void willRethrowIfTheExceptionMessageDoesNotMatchTheExpectedExceptionMessage() 71 | throws Throwable { 72 | givenExtensionContentWithMethod("canHandleARuntimeException"); 73 | 74 | RuntimeException expected = new RuntimeException(""); 75 | 76 | RuntimeException actual = 77 | assertThrows( 78 | expected.getClass(), 79 | () -> sut.handleTestExecutionException(extensionContext, expected)); 80 | assertThat(actual, is(expected)); 81 | } 82 | 83 | @Test 84 | public void willRethrowIfTheExceptionMessageDoesNotStartWithTheExpectedExceptionMessage() 85 | throws Throwable { 86 | givenExtensionContentWithMethod("canHandleAnExceptionWithAMessageWhichStartsWith"); 87 | 88 | RuntimeException expected = new RuntimeException("Foo"); 89 | 90 | RuntimeException actual = 91 | assertThrows( 92 | expected.getClass(), 93 | () -> sut.handleTestExecutionException(extensionContext, expected)); 94 | assertThat(actual, is(expected)); 95 | } 96 | 97 | @Test 98 | public void willRethrowIfTheExceptionMessageDoesNotContainTheExpectedExceptionMessage() 99 | throws Throwable { 100 | givenExtensionContentWithMethod("canHandleAnExceptionWithAMessageWhichContains"); 101 | 102 | RuntimeException expected = new RuntimeException("Bar"); 103 | 104 | RuntimeException actual = 105 | assertThrows( 106 | expected.getClass(), 107 | () -> sut.handleTestExecutionException(extensionContext, expected)); 108 | assertThat(actual, is(expected)); 109 | } 110 | 111 | /** 112 | * @throws Throwable 113 | * @see 114 | */ 115 | @Test 116 | public void willAssertFailureIfAnExceptionIsNeitherThrownNorHandled() throws Throwable { 117 | when(extensionContext.getStore(create(ExpectedExceptionExtension.class, extensionContext))) 118 | .thenReturn(store); 119 | 120 | // no exception was handled by the extension 121 | when(store.getOrComputeIfAbsent(any(String.class), any(Function.class))).thenReturn(false); 122 | 123 | // the extension context does not contain an exception 124 | when(extensionContext.getExecutionException()).thenReturn(Optional.empty()); 125 | 126 | AssertionFailedError actual = 127 | assertThrows(AssertionFailedError.class, () -> sut.afterTestExecution(extensionContext)); 128 | assertThat(actual.getMessage(), is("Expected an exception but no exception was thrown!")); 129 | } 130 | 131 | /** 132 | * @throws Throwable 133 | * @see 134 | */ 135 | @Test 136 | public void willNotAssertFailureIfTheExceptionContextContainsAnException() throws Throwable { 137 | when(extensionContext.getStore(create(ExpectedExceptionExtension.class, extensionContext))) 138 | .thenReturn(store); 139 | 140 | // no exception was handled by the extension 141 | when(store.getOrComputeIfAbsent(any(String.class), any(Function.class))).thenReturn(false); 142 | 143 | // the extension context contains an exception 144 | when(extensionContext.getExecutionException()).thenReturn(Optional.of(new Exception("boom!"))); 145 | 146 | sut.afterTestExecution(extensionContext); 147 | } 148 | 149 | /** 150 | * @throws Throwable 151 | * @see 152 | */ 153 | @Test 154 | public void willNotAssertFailureIfTheExceptionIsHandled() throws Throwable { 155 | when(extensionContext.getStore(create(ExpectedExceptionExtension.class, extensionContext))) 156 | .thenReturn(store); 157 | 158 | // an exception was handled by the extension 159 | when(store.getOrComputeIfAbsent(any(String.class), any(Function.class))).thenReturn(true); 160 | 161 | // the extension context does not contains an exception 162 | when(extensionContext.getExecutionException()).thenReturn(Optional.empty()); 163 | 164 | sut.afterTestExecution(extensionContext); 165 | } 166 | 167 | private void givenExtensionContentWithMethod(String methodName) throws NoSuchMethodException { 168 | when(extensionContext.getTestMethod()).thenReturn(Optional.of(getMethod(methodName))); 169 | } 170 | 171 | private Method getMethod(String methodName) throws NoSuchMethodException { 172 | return ExpectedExceptionExtensionTest.class.getMethod(methodName); 173 | } 174 | } -------------------------------------------------------------------------------- /src/test/java/io/github/glytching/junit/extension/exception/ExpectedExceptionExtensionTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.github.glytching.junit.extension.exception; 18 | 19 | import org.junit.jupiter.api.Test; 20 | 21 | public class ExpectedExceptionExtensionTest { 22 | 23 | @Test 24 | @ExpectedException(type = Throwable.class, messageIs = "Boom!") 25 | public void canHandleAThrowable() throws Throwable { 26 | throw new Throwable("Boom!"); 27 | } 28 | 29 | @Test 30 | @ExpectedException(type = Throwable.class, messageIs = "Boom!") 31 | public void canHandleASubClassOfTheExpectedExceptionType() throws Throwable { 32 | throw new Exception("Boom!"); 33 | } 34 | 35 | @Test 36 | @ExpectedException(type = Exception.class) 37 | public void canHandleAnExceptionWithAnyMessage() throws Throwable { 38 | throw new Exception("some message"); 39 | } 40 | 41 | @Test 42 | @ExpectedException(type = Exception.class, messageIs = "Boom!") 43 | public void canHandleAnException() throws Exception { 44 | throw new Exception("Boom!"); 45 | } 46 | 47 | @Test 48 | @ExpectedException(type = RuntimeException.class, messageIs = "Boom!") 49 | public void canHandleARuntimeException() { 50 | throw new RuntimeException("Boom!"); 51 | } 52 | 53 | @Test 54 | @ExpectedException(type = RuntimeException.class, messageStartsWith = "Bye") 55 | public void canHandleAnExceptionWithAMessageWhichStartsWith() { 56 | throw new RuntimeException("Bye bye"); 57 | } 58 | 59 | @Test 60 | @ExpectedException(type = RuntimeException.class, messageContains = "sorry") 61 | public void canHandleAnExceptionWithAMessageWhichContains() { 62 | throw new RuntimeException("Terribly sorry old chap"); 63 | } 64 | } -------------------------------------------------------------------------------- /src/test/java/io/github/glytching/junit/extension/folder/TemporaryFolderExtensionBeforeAllTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.github.glytching.junit.extension.folder; 18 | 19 | import org.junit.jupiter.api.AfterAll; 20 | import org.junit.jupiter.api.BeforeAll; 21 | import org.junit.jupiter.api.Test; 22 | import org.junit.jupiter.api.extension.ExtendWith; 23 | 24 | import java.io.File; 25 | import java.io.IOException; 26 | import java.nio.file.Files; 27 | import java.nio.file.Path; 28 | import java.util.Set; 29 | import java.util.stream.Collectors; 30 | import java.util.stream.Stream; 31 | 32 | import static org.hamcrest.MatcherAssert.assertThat; 33 | import static org.hamcrest.Matchers.hasItem; 34 | import static org.hamcrest.Matchers.is; 35 | 36 | @ExtendWith(TemporaryFolderExtension.class) 37 | public class TemporaryFolderExtensionBeforeAllTest { 38 | 39 | private static TemporaryFolder TEMPORARY_FOLDER; 40 | 41 | @BeforeAll 42 | public static void setUp(TemporaryFolder givenTemporaryFolder) { 43 | TEMPORARY_FOLDER = givenTemporaryFolder; 44 | } 45 | 46 | @AfterAll 47 | public static void cleanUp() throws IOException { 48 | try (Stream stream = Files.list(TEMPORARY_FOLDER.getRoot().toPath())) { 49 | Set createdFileNames = 50 | stream.map(path -> path.toFile().getName()).collect(Collectors.toSet()); 51 | 52 | // when using a static TemporaryFolder, every test gets the same instance so in AfterAll the 53 | // folder should contain all artifacts created by all tests in this test case 54 | assertThat(createdFileNames.size(), is(2)); 55 | assertThat(createdFileNames, hasItem("foo.txt")); 56 | assertThat(createdFileNames, hasItem("bar")); 57 | } 58 | } 59 | 60 | @Test 61 | public void canCreateAFile() throws IOException { 62 | File file = TEMPORARY_FOLDER.createFile("foo.txt"); 63 | assertThat(file.exists(), is(true)); 64 | } 65 | 66 | @Test 67 | public void canCreateADirectory() throws IOException { 68 | File file = TEMPORARY_FOLDER.createDirectory("bar"); 69 | assertThat(file.exists(), is(true)); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/test/java/io/github/glytching/junit/extension/folder/TemporaryFolderExtensionBeforeEachTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.github.glytching.junit.extension.folder; 18 | 19 | import org.junit.jupiter.api.AfterAll; 20 | import org.junit.jupiter.api.BeforeEach; 21 | import org.junit.jupiter.api.RepeatedTest; 22 | import org.junit.jupiter.api.Test; 23 | import org.junit.jupiter.api.extension.ExtendWith; 24 | 25 | import java.io.File; 26 | import java.io.IOException; 27 | import java.nio.file.Files; 28 | import java.nio.file.Paths; 29 | import java.util.HashSet; 30 | import java.util.List; 31 | import java.util.Set; 32 | import java.util.stream.Collectors; 33 | 34 | import static org.hamcrest.MatcherAssert.assertThat; 35 | import static org.hamcrest.Matchers.*; 36 | 37 | @ExtendWith(TemporaryFolderExtension.class) 38 | public class TemporaryFolderExtensionBeforeEachTest { 39 | 40 | // gather the temporary file and directory paths to facilitate assertions on (a) the distinct-ness 41 | // of the temporary folder address supplied to each test and (b) the removal of each temporary 42 | // folder on test completion 43 | private static final Set temporaryFilePaths = new HashSet<>(); 44 | private static final Set temporaryDirectoryPaths = new HashSet<>(); 45 | 46 | private TemporaryFolder temporaryFolder; 47 | 48 | @AfterAll 49 | public static void allTemporaryFilesAreDeleted() { 50 | List existingFiles = 51 | temporaryFilePaths 52 | .stream() 53 | .filter(temporaryFilePath -> Files.exists(Paths.get(temporaryFilePath))) 54 | .collect(Collectors.toList()); 55 | 56 | assertThat(existingFiles, empty()); 57 | } 58 | 59 | @AfterAll 60 | public static void allTemporaryDirectoriesAreDeleted() { 61 | List existingDirectories = 62 | temporaryDirectoryPaths 63 | .stream() 64 | .filter(temporaryFileDirectory -> Files.exists(Paths.get(temporaryFileDirectory))) 65 | .collect(Collectors.toList()); 66 | 67 | assertThat(existingDirectories, empty()); 68 | } 69 | 70 | @BeforeEach 71 | public void setUp(TemporaryFolder temporaryFolder) { 72 | this.temporaryFolder = temporaryFolder; 73 | } 74 | 75 | @Test 76 | public void canInjectATemporaryFolderAsAField() throws IOException { 77 | File file = temporaryFolder.createFile("foo.txt"); 78 | assertThat(file.exists(), is(true)); 79 | 80 | File dir = temporaryFolder.createDirectory("bar"); 81 | assertThat(dir.exists(), is(true)); 82 | } 83 | 84 | @Test 85 | public void canGetTheRootFolderWhenATemporaryFolderIsInjectedAsAField() throws IOException { 86 | File root = temporaryFolder.getRoot(); 87 | assertThat(root.exists(), is(true)); 88 | 89 | File dir = temporaryFolder.createDirectory("bar"); 90 | assertThat(dir.getParentFile(), is(root)); 91 | } 92 | 93 | @RepeatedTest(5) 94 | public void willCreateANewTemporaryFileEveryTime() throws IOException { 95 | File file = temporaryFolder.createFile("foo.txt"); 96 | assertThat(file.exists(), is(true)); 97 | 98 | if (temporaryFilePaths.isEmpty()) { 99 | temporaryFilePaths.add(file.getAbsolutePath()); 100 | } else { 101 | assertThat( 102 | "Received the same value twice, expected each random value to be different!", 103 | temporaryFilePaths, 104 | not(hasItem(file.getAbsolutePath()))); 105 | temporaryFilePaths.add(file.getAbsolutePath()); 106 | } 107 | } 108 | 109 | @RepeatedTest(5) 110 | public void willCreateANewTemporaryDirectoryEveryTime() throws IOException { 111 | File dir = temporaryFolder.createDirectory("bar"); 112 | assertThat(dir.exists(), is(true)); 113 | if (temporaryDirectoryPaths.isEmpty()) { 114 | temporaryDirectoryPaths.add(dir.getAbsolutePath()); 115 | } else { 116 | assertThat( 117 | "Received the same value twice, expected each random value to be different!", 118 | temporaryDirectoryPaths, 119 | not(hasItem(dir.getAbsolutePath()))); 120 | temporaryDirectoryPaths.add(dir.getAbsolutePath()); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/test/java/io/github/glytching/junit/extension/folder/TemporaryFolderExtensionParameterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.github.glytching.junit.extension.folder; 18 | 19 | import org.junit.jupiter.api.AfterAll; 20 | import org.junit.jupiter.api.RepeatedTest; 21 | import org.junit.jupiter.api.Test; 22 | import org.junit.jupiter.api.extension.ExtendWith; 23 | 24 | import java.io.File; 25 | import java.io.IOException; 26 | import java.nio.file.Files; 27 | import java.nio.file.Paths; 28 | import java.util.HashSet; 29 | import java.util.List; 30 | import java.util.Set; 31 | import java.util.stream.Collectors; 32 | 33 | import static org.hamcrest.MatcherAssert.assertThat; 34 | import static org.hamcrest.Matchers.*; 35 | 36 | public class TemporaryFolderExtensionParameterTest { 37 | 38 | // gather the temporary file and directory paths to facilitate assertions on (a) the distinct-ness 39 | // of the temporary folder address supplied to each test and (b) the removal of each temporary 40 | // folder on test completion 41 | private static final Set temporaryFilePaths = new HashSet<>(); 42 | private static final Set temporaryDirectoryPaths = new HashSet<>(); 43 | 44 | @AfterAll 45 | public static void allTemporaryFilesAreDeleted() { 46 | List existingFiles = 47 | temporaryFilePaths 48 | .stream() 49 | .filter(temporaryFilePath -> Files.exists(Paths.get(temporaryFilePath))) 50 | .collect(Collectors.toList()); 51 | 52 | assertThat(existingFiles, empty()); 53 | } 54 | 55 | @AfterAll 56 | public static void allTemporaryDirectoriesAreDeleted() { 57 | List existingDirectories = 58 | temporaryDirectoryPaths 59 | .stream() 60 | .filter(temporaryFileDirectory -> Files.exists(Paths.get(temporaryFileDirectory))) 61 | .collect(Collectors.toList()); 62 | 63 | assertThat(existingDirectories, empty()); 64 | } 65 | 66 | @Test 67 | @ExtendWith(TemporaryFolderExtension.class) 68 | public void canInjectATemporaryFolderAsAParameter(TemporaryFolder temporaryFolder) 69 | throws IOException { 70 | File file = temporaryFolder.createFile("foo.txt"); 71 | 72 | assertThat(file.exists(), is(true)); 73 | 74 | File dir = temporaryFolder.createDirectory("bar"); 75 | 76 | assertThat(dir.exists(), is(true)); 77 | } 78 | 79 | @Test 80 | @ExtendWith(TemporaryFolderExtension.class) 81 | public void canGetTheRootFolderWhenATemporaryFolderIsInjectedAsAParameter(TemporaryFolder temporaryFolder) 82 | throws IOException { 83 | File root = temporaryFolder.getRoot(); 84 | 85 | assertThat(root.exists(), is(true)); 86 | 87 | File dir = temporaryFolder.createDirectory("bar"); 88 | assertThat(dir.getParentFile(), is(root)); 89 | } 90 | 91 | @RepeatedTest(5) 92 | @ExtendWith(TemporaryFolderExtension.class) 93 | public void willCreateANewTemporaryFileEveryTime(TemporaryFolder temporaryFolder) 94 | throws IOException { 95 | File file = temporaryFolder.createFile("foo.txt"); 96 | 97 | assertThat(file.exists(), is(true)); 98 | 99 | if (temporaryFilePaths.isEmpty()) { 100 | temporaryFilePaths.add(file.getAbsolutePath()); 101 | } else { 102 | assertThat( 103 | "Received the same value twice, expected each random value to be different!", 104 | temporaryFilePaths, 105 | not(hasItem(file.getAbsolutePath()))); 106 | temporaryFilePaths.add(file.getAbsolutePath()); 107 | } 108 | } 109 | 110 | @RepeatedTest(5) 111 | @ExtendWith(TemporaryFolderExtension.class) 112 | public void willCreateANewTemporaryDirectoryEveryTime(TemporaryFolder temporaryFolder) { 113 | File dir = temporaryFolder.createDirectory("bar"); 114 | 115 | assertThat(dir.exists(), is(true)); 116 | 117 | if (temporaryDirectoryPaths.isEmpty()) { 118 | temporaryDirectoryPaths.add(dir.getAbsolutePath()); 119 | } else { 120 | assertThat( 121 | "Received the same value twice, expected each random value to be different!", 122 | temporaryDirectoryPaths, 123 | not(hasItem(dir.getAbsolutePath()))); 124 | temporaryDirectoryPaths.add(dir.getAbsolutePath()); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/test/java/io/github/glytching/junit/extension/folder/TemporaryFolderTest.java: -------------------------------------------------------------------------------- 1 | package io.github.glytching.junit.extension.folder; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.io.IOException; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertThrows; 8 | 9 | public class TemporaryFolderTest { 10 | 11 | @Test 12 | public void willThrowAnExceptionIfTheGivenDirectoryNameIsInvalid() throws IOException { 13 | TemporaryFolder temporaryFolder = new TemporaryFolder(); 14 | 15 | // should cover invalid characters on all platforms 16 | String invalidDirectoryName = "\\\\/:*?\\\"<>|/:"; 17 | assertThrows( 18 | TemporaryFolderException.class, 19 | () -> temporaryFolder.createDirectory(invalidDirectoryName)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/test/java/io/github/glytching/junit/extension/random/DomainObject.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.github.glytching.junit.extension.random; 18 | 19 | import java.util.List; 20 | 21 | public class DomainObject { 22 | 23 | private int id; 24 | private String name; 25 | private long value; 26 | private double price; 27 | private NestedDomainObject nestedDomainObject; 28 | private List wotsits; 29 | 30 | public DomainObject() {} 31 | 32 | public int getId() { 33 | return id; 34 | } 35 | 36 | public void setId(int id) { 37 | this.id = id; 38 | } 39 | 40 | public String getName() { 41 | return name; 42 | } 43 | 44 | public void setName(String name) { 45 | this.name = name; 46 | } 47 | 48 | public long getValue() { 49 | return value; 50 | } 51 | 52 | public void setValue(long value) { 53 | this.value = value; 54 | } 55 | 56 | public double getPrice() { 57 | return price; 58 | } 59 | 60 | public void setPrice(double price) { 61 | this.price = price; 62 | } 63 | 64 | public NestedDomainObject getNestedDomainObject() { 65 | return nestedDomainObject; 66 | } 67 | 68 | public void setNestedDomainObject(NestedDomainObject nestedDomainObject) { 69 | this.nestedDomainObject = nestedDomainObject; 70 | } 71 | 72 | public List getWotsits() { 73 | return wotsits; 74 | } 75 | 76 | public void setWotsits(List wotsits) { 77 | this.wotsits = wotsits; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/test/java/io/github/glytching/junit/extension/random/NestedDomainObject.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.github.glytching.junit.extension.random; 18 | 19 | public class NestedDomainObject { 20 | 21 | private String address; 22 | private String category; 23 | 24 | public NestedDomainObject() {} 25 | 26 | public String getAddress() { 27 | return address; 28 | } 29 | 30 | public void setAddress(String address) { 31 | this.address = address; 32 | } 33 | 34 | public String getCategory() { 35 | return category; 36 | } 37 | 38 | public void setCategory(String category) { 39 | this.category = category; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/java/io/github/glytching/junit/extension/random/RandomBeansExtensionFieldTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.github.glytching.junit.extension.random; 18 | 19 | import io.github.glytching.junit.extension.util.AssertionUtil; 20 | import org.junit.jupiter.api.RepeatedTest; 21 | import org.junit.jupiter.api.Test; 22 | import org.junit.jupiter.api.extension.ExtendWith; 23 | 24 | import java.util.Collection; 25 | import java.util.HashSet; 26 | import java.util.List; 27 | import java.util.Set; 28 | import java.util.stream.Stream; 29 | 30 | import static io.github.glytching.junit.extension.util.AssertionUtil.*; 31 | import static org.hamcrest.MatcherAssert.assertThat; 32 | import static org.hamcrest.Matchers.*; 33 | 34 | @ExtendWith(RandomBeansExtension.class) 35 | public class RandomBeansExtensionFieldTest { 36 | 37 | @Random private static String anyStaticString; 38 | 39 | // gather the random values to facilitate assertions on the not distinct-ness of the value supplied to 40 | // each test 41 | private static Collection anyStaticStrings = new HashSet<>(); 42 | 43 | // gather the random values to facilitate assertions on the distinct-ness of the value supplied to 44 | // each test 45 | private final Set anyStrings = new HashSet<>(); 46 | 47 | @Random private String anyString; 48 | 49 | @Random private DomainObject fullyPopulatedDomainObject; 50 | 51 | @Random(excludes = {"wotsits", "id", "nestedDomainObject.address"}) 52 | private DomainObject partiallyPopulatedDomainObject; 53 | 54 | @Random(type = String.class) 55 | private List anyList; 56 | 57 | @Random(size = 5, type = String.class) 58 | private List anyListOfSpecificSize; 59 | 60 | @Random(type = String.class) 61 | private Set anySet; 62 | 63 | @Random(type = String.class) 64 | private Stream anyStream; 65 | 66 | @Random(type = String.class) 67 | private Collection anyCollection; 68 | 69 | @Random(size = 2, type = DomainObject.class) 70 | private List anyFullyPopulatedDomainObjects; 71 | 72 | @Random( 73 | size = 2, 74 | type = DomainObject.class, 75 | excludes = {"wotsits", "id", "nestedDomainObject.address"} 76 | ) 77 | private List anyPartiallyPopulatedDomainObjects; 78 | 79 | @Test 80 | public void canInjectARandomString() { 81 | assertThat(anyString, notNullValue()); 82 | } 83 | 84 | @Test 85 | public void canInjectAFullyPopulatedRandomObject() { 86 | assertThatDomainObjectIsFullyPopulated(fullyPopulatedDomainObject); 87 | } 88 | 89 | @Test 90 | public void canInjectStaticFields() { 91 | assertThat(anyStaticString, is(notNullValue())); 92 | } 93 | 94 | @Test 95 | public void canInjectAPartiallyPopulatedRandomObject() { 96 | assertThatDomainObjectIsPartiallyPopulated(partiallyPopulatedDomainObject); 97 | } 98 | 99 | @Test 100 | public void canInjectARandomListOfDefaultSize() throws Exception { 101 | assertThat(anyList, notNullValue()); 102 | assertThat(anyList, not(empty())); 103 | assertThat(anyList.size(), is(getDefaultSizeOfRandom())); 104 | } 105 | 106 | @Test 107 | public void canInjectARandomListOfSpecificSize() { 108 | assertThat(anyListOfSpecificSize, notNullValue()); 109 | assertThat(anyListOfSpecificSize.size(), is(5)); 110 | } 111 | 112 | @Test 113 | public void canInjectARandomSet() throws Exception { 114 | assertThat(anySet, notNullValue()); 115 | assertThat(anySet, not(empty())); 116 | assertThat(anySet.size(), is(getDefaultSizeOfRandom())); 117 | } 118 | 119 | @Test 120 | public void canInjectARandomStream() throws Exception { 121 | assertThat(anyStream, notNullValue()); 122 | //noinspection UnnecessaryBoxing 123 | assertThat(anyStream.count(), is(Long.valueOf(getDefaultSizeOfRandom()))); 124 | } 125 | 126 | @Test 127 | public void canInjectARandomCollection() throws Exception { 128 | assertThat(anyCollection, notNullValue()); 129 | assertThat(anyCollection, not(empty())); 130 | assertThat(anyCollection.size(), is(getDefaultSizeOfRandom())); 131 | } 132 | 133 | @Test 134 | public void canInjectRandomFullyPopulatedDomainObjects() { 135 | assertThat(anyFullyPopulatedDomainObjects, notNullValue()); 136 | assertThat(anyFullyPopulatedDomainObjects.size(), is(2)); 137 | anyFullyPopulatedDomainObjects.forEach(AssertionUtil::assertThatDomainObjectIsFullyPopulated); 138 | } 139 | 140 | @Test 141 | public void canInjectRandomPartiallyPopulatedDomainObjects() { 142 | assertThat(anyPartiallyPopulatedDomainObjects, notNullValue()); 143 | assertThat(anyPartiallyPopulatedDomainObjects.size(), is(2)); 144 | anyPartiallyPopulatedDomainObjects.forEach( 145 | AssertionUtil::assertThatDomainObjectIsPartiallyPopulated); 146 | } 147 | 148 | @RepeatedTest(5) 149 | public void willInjectANewRandomValueEachTime() { 150 | assertThat(anyString, notNullValue()); 151 | 152 | if (anyStrings.isEmpty()) { 153 | anyStrings.add(anyString); 154 | } else { 155 | assertThat( 156 | "Received the same value twice, expected each random value to be different!", 157 | anyStrings, 158 | not(hasItem(anyString))); 159 | anyStrings.add(anyString); 160 | } 161 | } 162 | 163 | @RepeatedTest(5) 164 | public void willNotInjectANewRandomValueEachTimeForAStaticField() { 165 | assertThat(anyStaticString, notNullValue()); 166 | anyStaticStrings.add(anyStaticString); 167 | assertThat(anyStaticStrings, is(hasSize(1))); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/test/java/io/github/glytching/junit/extension/random/RandomBeansExtensionOverrideTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.github.glytching.junit.extension.random; 18 | 19 | import io.github.benas.randombeans.EnhancedRandomBuilder; 20 | import io.github.benas.randombeans.FieldDefinitionBuilder; 21 | import io.github.benas.randombeans.api.EnhancedRandom; 22 | import io.github.benas.randombeans.randomizers.range.DoubleRangeRandomizer; 23 | import io.github.benas.randombeans.randomizers.range.IntegerRangeRandomizer; 24 | import io.github.benas.randombeans.randomizers.text.StringRandomizer; 25 | import org.junit.jupiter.api.DisplayName; 26 | import org.junit.jupiter.api.RepeatedTest; 27 | import org.junit.jupiter.api.Test; 28 | import org.junit.jupiter.api.extension.RegisterExtension; 29 | 30 | import java.util.Collection; 31 | import java.util.HashSet; 32 | import java.util.List; 33 | import java.util.Set; 34 | import java.util.stream.Stream; 35 | 36 | import static io.github.glytching.junit.extension.util.AssertionUtil.getDefaultSizeOfRandom; 37 | import static org.hamcrest.MatcherAssert.assertThat; 38 | import static org.hamcrest.Matchers.*; 39 | 40 | public class RandomBeansExtensionOverrideTest { 41 | 42 | static EnhancedRandom enhancedRandom = EnhancedRandomBuilder 43 | .aNewEnhancedRandomBuilder() 44 | .exclude(FieldDefinitionBuilder 45 | .field() 46 | .named("wotsits") 47 | .ofType(List.class) 48 | .inClass(DomainObject.class) 49 | .get()) 50 | .randomize(Integer.class, IntegerRangeRandomizer.aNewIntegerRangeRandomizer(0, 10)) 51 | .randomize(String.class, StringRandomizer.aNewStringRandomizer(5)) 52 | .randomize(Double.class, DoubleRangeRandomizer.aNewDoubleRangeRandomizer(0.0, 10.0)) 53 | .build(); 54 | 55 | @RegisterExtension 56 | static RandomBeansExtension randomBeansExtension = new RandomBeansExtension(enhancedRandom); 57 | 58 | // gather the random values to facilitate assertions on the distinct-ness of the value supplied to 59 | // each test 60 | private final Set anyStrings = new HashSet<>(); 61 | 62 | @Test 63 | @RepeatedTest(5) 64 | public void canOverrideDefaultIntegerRangeByProgrammaticExtensionRegistration( 65 | @Random(type = Integer.class) Integer anyInteger) throws Exception { 66 | assertThat(anyInteger, notNullValue()); 67 | assertThat(anyInteger, lessThanOrEqualTo(10)); 68 | assertThat(anyInteger, greaterThanOrEqualTo(0)); 69 | } 70 | 71 | @Test 72 | @RepeatedTest(5) 73 | public void canOverrideDefaultDoubleRangeByProgrammaticExtensionRegistration( 74 | @Random(type = Double.class) Double anyDouble) throws Exception { 75 | assertThat(anyDouble, notNullValue()); 76 | assertThat(anyDouble, lessThanOrEqualTo(10.0)); 77 | assertThat(anyDouble, greaterThanOrEqualTo(0.0)); 78 | } 79 | 80 | @Test 81 | @RepeatedTest(5) 82 | public void canOverrideDefaultMaxLengthOfStringByProgrammaticExtensionRegistration( 83 | @Random(type = String.class) String anySting) throws Exception { 84 | assertThat(anySting, notNullValue()); 85 | assertThat(anySting.length(), lessThanOrEqualTo(5)); 86 | } 87 | 88 | @Test 89 | @DisplayName("Should Inject Random values with random bean default behaviour if no Overrides are provided") 90 | public void canInjectAttributesIfNoOverridesAreProvided(@Random Long someRandomLong) throws Exception { 91 | assertThat(someRandomLong, notNullValue()); 92 | } 93 | 94 | @Test 95 | public void canInjectAPartiallyPopulatedRandomObjectWithProgrammaticExtensionRegistration(@Random DomainObject domainObject) { 96 | assertThat(domainObject.getWotsits(), notNullValue()); 97 | assertThat(domainObject.getWotsits(), not(empty())); 98 | } 99 | 100 | @Test 101 | public void canInjectARandomListOfDefaultSizeWithProgrammaticExtensionRegistration(@Random(type = String.class) List anyList) 102 | throws Exception { 103 | assertThat(anyList, notNullValue()); 104 | assertThat(anyList, not(empty())); 105 | assertThat(anyList.size(), is(getDefaultSizeOfRandom())); 106 | } 107 | 108 | @Test 109 | public void canInjectARandomListOfSpecificSizeWithProgrammaticExtensionRegistration( 110 | @Random(size = 5, type = String.class) List anyListOfSpecificSize) { 111 | assertThat(anyListOfSpecificSize, notNullValue()); 112 | assertThat(anyListOfSpecificSize.size(), is(5)); 113 | } 114 | 115 | @Test 116 | public void canInjectARandomSetWithProgrammaticExtensionRegistration(@Random(type = String.class) Set anySet) 117 | throws Exception { 118 | assertThat(anySet, notNullValue()); 119 | assertThat(anySet, not(empty())); 120 | assertThat(anySet.size(), is(getDefaultSizeOfRandom())); 121 | } 122 | 123 | @Test 124 | public void canInjectARandomStreamWithProgrammaticExtensionRegistration(@Random(type = String.class) Stream anyStream) 125 | throws Exception { 126 | assertThat(anyStream, notNullValue()); 127 | //noinspection UnnecessaryBoxing 128 | assertThat(anyStream.count(), is(Long.valueOf(getDefaultSizeOfRandom()))); 129 | } 130 | 131 | @Test 132 | public void canInjectARandomCollectionWithProgrammaticExtensionRegistration( 133 | @Random(type = String.class) Collection anyCollection) throws Exception { 134 | assertThat(anyCollection, notNullValue()); 135 | assertThat(anyCollection, not(empty())); 136 | assertThat(anyCollection.size(), is(getDefaultSizeOfRandom())); 137 | } 138 | 139 | @RepeatedTest(5) 140 | public void willInjectANewRandomValueEachTimeWithProgrammaticExtensionRegistration(@Random String anyString) { 141 | System.out.println(anyString); 142 | assertThat(anyString, notNullValue()); 143 | 144 | if (anyStrings.isEmpty()) { 145 | anyStrings.add(anyString); 146 | } else { 147 | assertThat( 148 | "Received the same value twice, expected each random value to be different!", 149 | anyStrings, 150 | not(hasItem(anyString))); 151 | anyStrings.add(anyString); 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/test/java/io/github/glytching/junit/extension/random/RandomBeansExtensionParameterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.github.glytching.junit.extension.random; 18 | 19 | import io.github.glytching.junit.extension.util.AssertionUtil; 20 | import org.junit.jupiter.api.RepeatedTest; 21 | import org.junit.jupiter.api.Test; 22 | import org.junit.jupiter.api.extension.ExtendWith; 23 | 24 | import java.util.Collection; 25 | import java.util.HashSet; 26 | import java.util.List; 27 | import java.util.Set; 28 | import java.util.stream.Stream; 29 | 30 | import static io.github.glytching.junit.extension.util.AssertionUtil.*; 31 | import static org.hamcrest.MatcherAssert.assertThat; 32 | import static org.hamcrest.Matchers.*; 33 | 34 | @ExtendWith(RandomBeansExtension.class) 35 | public class RandomBeansExtensionParameterTest { 36 | 37 | // gather the random values to facilitate assertions on the distinct-ness of the value supplied to 38 | // each test 39 | private final Set anyStrings = new HashSet<>(); 40 | 41 | @Test 42 | @ExtendWith(RandomBeansExtension.class) 43 | public void canInjectARandomString(@Random String anyString) { 44 | assertThat(anyString, notNullValue()); 45 | } 46 | 47 | @Test 48 | public void canInjectAFullyPopulatedRandomObject(@Random DomainObject domainObject) { 49 | assertThatDomainObjectIsFullyPopulated(domainObject); 50 | } 51 | 52 | @Test 53 | public void canInjectAPartiallyPopulatedRandomObject( 54 | @Random(excludes = {"wotsits", "id", "nestedDomainObject.address"}) 55 | DomainObject domainObject) { 56 | assertThatDomainObjectIsPartiallyPopulated(domainObject); 57 | } 58 | 59 | @Test 60 | public void canInjectARandomListOfDefaultSize(@Random(type = String.class) List anyList) 61 | throws Exception { 62 | assertThat(anyList, notNullValue()); 63 | assertThat(anyList, not(empty())); 64 | assertThat(anyList.size(), is(getDefaultSizeOfRandom())); 65 | } 66 | 67 | @Test 68 | public void canInjectARandomListOfSpecificSize( 69 | @Random(size = 5, type = String.class) List anyListOfSpecificSize) { 70 | assertThat(anyListOfSpecificSize, notNullValue()); 71 | assertThat(anyListOfSpecificSize.size(), is(5)); 72 | } 73 | 74 | @Test 75 | public void canInjectARandomSet(@Random(type = String.class) Set anySet) 76 | throws Exception { 77 | assertThat(anySet, notNullValue()); 78 | assertThat(anySet, not(empty())); 79 | assertThat(anySet.size(), is(getDefaultSizeOfRandom())); 80 | } 81 | 82 | @Test 83 | public void canInjectARandomStream(@Random(type = String.class) Stream anyStream) 84 | throws Exception { 85 | assertThat(anyStream, notNullValue()); 86 | //noinspection UnnecessaryBoxing 87 | assertThat(anyStream.count(), is(Long.valueOf(getDefaultSizeOfRandom()))); 88 | } 89 | 90 | @Test 91 | public void canInjectARandomCollection( 92 | @Random(type = String.class) Collection anyCollection) throws Exception { 93 | assertThat(anyCollection, notNullValue()); 94 | assertThat(anyCollection, not(empty())); 95 | assertThat(anyCollection.size(), is(getDefaultSizeOfRandom())); 96 | } 97 | 98 | @Test 99 | public void canInjectRandomFullyPopulatedDomainObjects( 100 | @Random(size = 2, type = DomainObject.class) 101 | List anyFullyPopulatedDomainObjects) { 102 | assertThat(anyFullyPopulatedDomainObjects, notNullValue()); 103 | assertThat(anyFullyPopulatedDomainObjects.size(), is(2)); 104 | anyFullyPopulatedDomainObjects.forEach(AssertionUtil::assertThatDomainObjectIsFullyPopulated); 105 | } 106 | 107 | @Test 108 | public void canInjectRandomPartiallyPopulatedDomainObjects( 109 | @Random( 110 | size = 2, 111 | type = DomainObject.class, 112 | excludes = {"wotsits", "id", "nestedDomainObject.address"} 113 | ) 114 | List anyPartiallyPopulatedDomainObjects) { 115 | assertThat(anyPartiallyPopulatedDomainObjects, notNullValue()); 116 | assertThat(anyPartiallyPopulatedDomainObjects.size(), is(2)); 117 | anyPartiallyPopulatedDomainObjects.forEach( 118 | AssertionUtil::assertThatDomainObjectIsPartiallyPopulated); 119 | } 120 | 121 | @RepeatedTest(5) 122 | @ExtendWith(RandomBeansExtension.class) 123 | public void willInjectANewRandomValueEachTime(@Random String anyString) { 124 | assertThat(anyString, notNullValue()); 125 | 126 | if (anyStrings.isEmpty()) { 127 | anyStrings.add(anyString); 128 | } else { 129 | assertThat( 130 | "Received the same value twice, expected each random value to be different!", 131 | anyStrings, 132 | not(hasItem(anyString))); 133 | anyStrings.add(anyString); 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/test/java/io/github/glytching/junit/extension/system/SystemPropertyExtensionClassTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.github.glytching.junit.extension.system; 18 | 19 | import org.junit.jupiter.api.Test; 20 | 21 | import static org.hamcrest.MatcherAssert.assertThat; 22 | import static org.hamcrest.Matchers.is; 23 | 24 | @SystemProperty(name = "classPropertyKeyC", value = "classPropertyValueC") 25 | @SystemProperty(name = "classPropertyKeyA", value = "classPropertyValueA") 26 | @SystemProperty(name = "classPropertyKeyB", value = "classPropertyValueB") 27 | public class SystemPropertyExtensionClassTest { 28 | 29 | @Test 30 | public void canSetSystemPropertyAtClassLevel() { 31 | assertThat(System.getProperty("classPropertyKeyC"), is("classPropertyValueC")); 32 | } 33 | 34 | @Test 35 | public void canSetSystemPropertiesAtClassLevel() { 36 | assertThat(System.getProperty("classPropertyKeyA"), is("classPropertyValueA")); 37 | assertThat(System.getProperty("classPropertyKeyB"), is("classPropertyValueB")); 38 | } 39 | 40 | @Test 41 | @SystemProperty(name = "keyA", value = "valueA") 42 | public void canHandleClassAndMethodLevelSystemProperties() { 43 | assertThat(System.getProperty("classPropertyKeyA"), is("classPropertyValueA")); 44 | assertThat(System.getProperty("classPropertyKeyB"), is("classPropertyValueB")); 45 | assertThat(System.getProperty("classPropertyKeyC"), is("classPropertyValueC")); 46 | 47 | assertThat(System.getProperty("keyA"), is("valueA")); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/test/java/io/github/glytching/junit/extension/system/SystemPropertyExtensionMetaTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.github.glytching.junit.extension.system; 18 | 19 | import org.junit.jupiter.api.Test; 20 | 21 | import static io.github.glytching.junit.extension.util.ExtensionTester.execute; 22 | import static org.hamcrest.MatcherAssert.assertThat; 23 | import static org.hamcrest.Matchers.is; 24 | import static org.hamcrest.Matchers.nullValue; 25 | import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; 26 | import static org.junit.platform.engine.discovery.DiscoverySelectors.selectMethod; 27 | 28 | /** 29 | * Tests the {@link SystemPropertyExtension} from the outside. There are other tests which 30 | * verify that the 'set property' side effects of the {@link SystemPropertyExtension} are correct 31 | * but this extension is also responsible for reverting whatever properties it set and we cannot 32 | * test that behaviour in the standard test flow. Instead, we have to run the test and then assert 33 | * that the reversion is successful after the test engine has completed (including invoking 34 | * afterEach, afterAll). 35 | */ 36 | public class SystemPropertyExtensionMetaTest { 37 | 38 | @Test 39 | public void classLevelSystemPropertyWillBeResetBackToTheirPreTestValue() { 40 | System.setProperty("classPropertyKeyC", "no"); 41 | 42 | execute(selectClass(SystemPropertyExtensionClassTest.class)); 43 | 44 | assertThat(System.getProperty("classPropertyKeyC"), is("no")); 45 | } 46 | 47 | @Test 48 | public void classLevelSystemPropertiesWillBeResetBackToTheirPreTestValue() { 49 | System.setProperty("classPropertyKeyA", "no"); 50 | 51 | execute(selectClass(SystemPropertyExtensionClassTest.class)); 52 | 53 | assertThat(System.getProperty("classPropertyKeyA"), is("no")); 54 | } 55 | 56 | @Test 57 | public void classLevelSystemPropertiesWillBeUnsetIfTheyHadNoPreTestValue() { 58 | execute(selectClass(SystemPropertyExtensionClassTest.class)); 59 | 60 | assertThat(System.getProperty("classPropertyKeyB"), nullValue()); 61 | } 62 | 63 | @Test 64 | public void methodLevelSystemPropertyWillBeResetBackToItsPreTestValue() { 65 | System.setProperty("keyC", "no"); 66 | 67 | execute(selectMethod(SystemPropertyExtensionMethodTest.class, "canSetSystemProperty")); 68 | 69 | assertThat(System.getProperty("keyC"), is("no")); 70 | } 71 | 72 | @Test 73 | public void methodLevelSystemPropertiesWillBeResetBackToTheirPreTestValues() { 74 | System.setProperty("keyA", "no"); 75 | 76 | execute(selectMethod(SystemPropertyExtensionMethodTest.class, "canSetSystemProperties")); 77 | 78 | assertThat(System.getProperty("keyA"), is("no")); 79 | } 80 | 81 | @Test 82 | public void methodLevelSystemPropertiesWillBeUnsetIfTheyHadNoPreTestValue() { 83 | execute(selectMethod(SystemPropertyExtensionMethodTest.class, "canSetSystemProperties")); 84 | 85 | assertThat(System.getProperty("keyB"), nullValue()); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/test/java/io/github/glytching/junit/extension/system/SystemPropertyExtensionMethodTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.github.glytching.junit.extension.system; 18 | 19 | import org.junit.jupiter.api.Test; 20 | 21 | import static org.hamcrest.MatcherAssert.assertThat; 22 | import static org.hamcrest.Matchers.is; 23 | 24 | public class SystemPropertyExtensionMethodTest { 25 | 26 | @Test 27 | @SystemProperty(name = "keyC", value = "valueC") 28 | public void canSetSystemProperty() { 29 | assertThat(System.getProperty("keyC"), is("valueC")); 30 | } 31 | 32 | @Test 33 | @SystemProperty(name = "keyA", value = "valueA") 34 | @SystemProperty(name = "keyB", value = "valueB") 35 | public void canSetSystemProperties() { 36 | assertThat(System.getProperty("keyA"), is("valueA")); 37 | assertThat(System.getProperty("keyB"), is("valueB")); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/test/java/io/github/glytching/junit/extension/testname/TestNameExtensionTest.java: -------------------------------------------------------------------------------- 1 | package io.github.glytching.junit.extension.testname; 2 | 3 | import org.junit.jupiter.api.AfterEach; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.Test; 6 | import org.junit.jupiter.api.extension.ExtendWith; 7 | 8 | import static org.hamcrest.MatcherAssert.assertThat; 9 | import static org.hamcrest.Matchers.is; 10 | import static org.hamcrest.Matchers.nullValue; 11 | 12 | @ExtendWith(TestNameExtension.class) 13 | public class TestNameExtensionTest { 14 | 15 | @TestName private String testName; 16 | 17 | @BeforeEach 18 | public void testNameIsOnlyPopulatedOnTestInvocation() { 19 | assertThat(testName, nullValue()); 20 | } 21 | 22 | @AfterEach 23 | public void testNameIsDiscardedBetweenTests() { 24 | assertThat(testName, nullValue()); 25 | } 26 | 27 | @Test 28 | public void canSetTestName() { 29 | assertThat(testName, is("canSetTestName")); 30 | } 31 | 32 | @Test 33 | public void canSetADifferentTestName() { 34 | assertThat(testName, is("canSetADifferentTestName")); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/io/github/glytching/junit/extension/util/AssertionUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.github.glytching.junit.extension.util; 18 | 19 | import io.github.glytching.junit.extension.random.DomainObject; 20 | import io.github.glytching.junit.extension.random.Random; 21 | 22 | import java.lang.reflect.Method; 23 | 24 | import static org.hamcrest.MatcherAssert.assertThat; 25 | import static org.hamcrest.Matchers.*; 26 | 27 | public class AssertionUtil { 28 | 29 | public static void assertThatDomainObjectIsFullyPopulated(DomainObject domainObject) { 30 | assertThat(domainObject, notNullValue()); 31 | 32 | assertThat(domainObject.getId(), notNullValue()); 33 | assertThat(domainObject.getId(), not(is(0))); 34 | 35 | assertThat(domainObject.getName(), notNullValue()); 36 | assertThat(domainObject.getName(), not(isEmptyString())); 37 | 38 | assertThat(domainObject.getNestedDomainObject(), notNullValue()); 39 | assertThat(domainObject.getNestedDomainObject().getAddress(), notNullValue()); 40 | assertThat(domainObject.getNestedDomainObject().getCategory(), notNullValue()); 41 | 42 | assertThat(domainObject.getWotsits(), notNullValue()); 43 | assertThat(domainObject.getWotsits(), not(empty())); 44 | 45 | assertThat(domainObject.getValue(), notNullValue()); 46 | assertThat(domainObject.getValue(), not(is(0L))); 47 | 48 | assertThat(domainObject.getPrice(), notNullValue()); 49 | assertThat(domainObject.getPrice(), not(is(0d))); 50 | } 51 | 52 | public static void assertThatDomainObjectIsPartiallyPopulated(DomainObject domainObject) { 53 | assertThat(domainObject, notNullValue()); 54 | 55 | assertThat(domainObject.getId(), is(0)); 56 | 57 | assertThat(domainObject.getName(), notNullValue()); 58 | assertThat(domainObject.getName(), not(isEmptyString())); 59 | 60 | assertThat(domainObject.getNestedDomainObject(), notNullValue()); 61 | assertThat(domainObject.getNestedDomainObject().getAddress(), nullValue()); 62 | assertThat(domainObject.getNestedDomainObject().getCategory(), notNullValue()); 63 | 64 | assertThat(domainObject.getWotsits(), nullValue()); 65 | 66 | assertThat(domainObject.getValue(), notNullValue()); 67 | assertThat(domainObject.getValue(), not(is(0L))); 68 | 69 | assertThat(domainObject.getPrice(), notNullValue()); 70 | assertThat(domainObject.getPrice(), not(is(0d))); 71 | } 72 | 73 | public static int getDefaultSizeOfRandom() throws Exception { 74 | Class clazz = Random.class; 75 | Method method = clazz.getDeclaredMethod("size"); 76 | return (Integer) method.getDefaultValue(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/test/java/io/github/glytching/junit/extension/util/ExecutionEvent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.github.glytching.junit.extension.util; 18 | 19 | import org.junit.platform.engine.TestDescriptor; 20 | import org.junit.platform.engine.TestExecutionResult; 21 | import org.junit.platform.engine.reporting.ReportEntry; 22 | 23 | import java.util.Optional; 24 | import java.util.function.Predicate; 25 | 26 | import static java.util.function.Predicate.isEqual; 27 | import static io.github.glytching.junit.extension.util.ExecutionEvent.Type.*; 28 | import static org.junit.platform.commons.util.FunctionUtils.where; 29 | 30 | /** 31 | * Represents an event collected by {@link RecordingExecutionListener}. The listener receives 32 | * callbacks from the engine during test execution, these callbacks represent stages in the test 33 | * execution lifecycle and are accompanied by the state which is relevant to each stage. The 34 | * intention here is to gather this state for use by assertions in the extension tests. 35 | * 36 | *

Note: this is mostly lifted from JUnit5's own (unpublished) test utilities. 37 | * 38 | * @see JUnit5 40 | * Test Utilities 41 | */ 42 | public class ExecutionEvent { 43 | 44 | private final ExecutionEvent.Type type; 45 | private final TestDescriptor testDescriptor; 46 | private final Object payload; 47 | 48 | private ExecutionEvent(ExecutionEvent.Type type, TestDescriptor testDescriptor, Object payload) { 49 | this.type = type; 50 | this.testDescriptor = testDescriptor; 51 | this.payload = payload; 52 | } 53 | 54 | public static ExecutionEvent reportingEntryPublished( 55 | TestDescriptor testDescriptor, ReportEntry entry) { 56 | return new ExecutionEvent(REPORTING_ENTRY_PUBLISHED, testDescriptor, entry); 57 | } 58 | 59 | public static ExecutionEvent dynamicTestRegistered(TestDescriptor testDescriptor) { 60 | return new ExecutionEvent(DYNAMIC_TEST_REGISTERED, testDescriptor, null); 61 | } 62 | 63 | public static ExecutionEvent executionSkipped(TestDescriptor testDescriptor, String reason) { 64 | return new ExecutionEvent(SKIPPED, testDescriptor, reason); 65 | } 66 | 67 | public static ExecutionEvent executionStarted(TestDescriptor testDescriptor) { 68 | return new ExecutionEvent(STARTED, testDescriptor, null); 69 | } 70 | 71 | public static ExecutionEvent executionFinished( 72 | TestDescriptor testDescriptor, TestExecutionResult result) { 73 | return new ExecutionEvent(FINISHED, testDescriptor, result); 74 | } 75 | 76 | public static Predicate byType(ExecutionEvent.Type type) { 77 | return where(ExecutionEvent::getType, isEqual(type)); 78 | } 79 | 80 | public static Predicate byTestDescriptor( 81 | Predicate predicate) { 82 | return where(ExecutionEvent::getTestDescriptor, predicate); 83 | } 84 | 85 | public static Predicate byPayload( 86 | Class payloadClass, Predicate predicate) { 87 | return event -> event.getPayload(payloadClass).filter(predicate).isPresent(); 88 | } 89 | 90 | public ExecutionEvent.Type getType() { 91 | return type; 92 | } 93 | 94 | public TestDescriptor getTestDescriptor() { 95 | return testDescriptor; 96 | } 97 | 98 | public Optional getPayload(Class payloadClass) { 99 | return Optional.ofNullable(payload).filter(payloadClass::isInstance).map(payloadClass::cast); 100 | } 101 | 102 | @Override 103 | public String toString() { 104 | return "ExecutionEvent{" 105 | + "type=" 106 | + type 107 | + ", testDescriptor=" 108 | + testDescriptor 109 | + ", payload=" 110 | + payload 111 | + '}'; 112 | } 113 | 114 | public enum Type { 115 | DYNAMIC_TEST_REGISTERED, 116 | SKIPPED, 117 | STARTED, 118 | FINISHED, 119 | REPORTING_ENTRY_PUBLISHED 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/test/java/io/github/glytching/junit/extension/util/ExtensionTester.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.github.glytching.junit.extension.util; 18 | 19 | import org.junit.jupiter.engine.JupiterTestEngine; 20 | import org.junit.platform.engine.DiscoverySelector; 21 | import org.junit.platform.engine.ExecutionRequest; 22 | import org.junit.platform.engine.TestDescriptor; 23 | import org.junit.platform.engine.UniqueId; 24 | import org.junit.platform.launcher.LauncherDiscoveryRequest; 25 | 26 | import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request; 27 | 28 | /** 29 | * A test utility which invokes the Jupiter engine for a given {@link DiscoverySelector} or {@link 30 | * DiscoverySelector}s. Typically, the {@code selectors} will isolate test class or test methods. 31 | * 32 | *

Why do we need this? can't we just tests extensions in the same way as we test any other 33 | * class? Many of the extension behaviours can be tested via side effects, for example: 34 | * 35 | *

    36 | *
  • An extension sets a system property: this can be asserted against in a test case 37 | *
  • An extension provides a temporary folder; a test case can use this temporary folder and 38 | * assert that it behaves correctly 39 | *
  • An extension injects random values; a test case can assert that these values are populated 40 | *
  • An extension catches expected exceptions; a test case can throw exceptions and assert that 41 | * they are correctly handled by the extension 42 | *
  • ... etc 43 | *
44 | * 45 | * But ... some extension behaviours cannot be tested in the normal test execution flow. For 46 | * example: 47 | * 48 | *
    49 | *
  • An extension which rethrows (rather than handles and swallows) an exception 50 | *
  • An extension which performs some {@link org.junit.jupiter.api.AfterAll} or {@link 51 | * org.junit.jupiter.api.AfterEach} cleanup 52 | *
  • An extension which has no discernible side effect e.g. one which only logs test execution 53 | *
54 | * 55 | * So, we'll need to execute some extensions within a JUnit container and then assert against what 56 | * the container did rather than only asserting against the side effects of the extension. 57 | */ 58 | public class ExtensionTester { 59 | 60 | /** 61 | * Instance an engine and execute the test resources identified by the given {@code selectors} and 62 | * wrap the response in a listener so that we can make sense of what happened. The listener 63 | * exposes information about the test execution flow which the extension tests can assert against. 64 | * 65 | * @param selectors {@link DiscoverySelector} instances which will isolate test class or test 66 | * methods 67 | * @return a {@link RecordingExecutionListener} which encapsulates what the engine did 68 | */ 69 | public static RecordingExecutionListener execute(DiscoverySelector... selectors) { 70 | // instance an engine 71 | JupiterTestEngine testEngine = new JupiterTestEngine(); 72 | 73 | // discover the requested test resources 74 | LauncherDiscoveryRequest discoveryRequest = request().selectors(selectors).build(); 75 | 76 | RecordingExecutionListener listener = new RecordingExecutionListener(); 77 | 78 | // execute the discovered test resources 79 | TestDescriptor testDescriptor = 80 | testEngine.discover(discoveryRequest, UniqueId.forEngine(testEngine.getId())); 81 | testEngine.execute( 82 | new ExecutionRequest( 83 | testDescriptor, listener, discoveryRequest.getConfigurationParameters())); 84 | 85 | return listener; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/test/java/io/github/glytching/junit/extension/util/RecordingExecutionListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.github.glytching.junit.extension.util; 18 | 19 | import io.github.glytching.junit.extension.util.ExecutionEvent.*; 20 | import org.junit.platform.engine.EngineExecutionListener; 21 | import org.junit.platform.engine.TestDescriptor; 22 | import org.junit.platform.engine.TestExecutionResult; 23 | import org.junit.platform.engine.TestExecutionResult.Status; 24 | import org.junit.platform.engine.reporting.ReportEntry; 25 | 26 | import java.util.List; 27 | import java.util.concurrent.CopyOnWriteArrayList; 28 | import java.util.function.Predicate; 29 | import java.util.stream.Stream; 30 | 31 | import static java.util.function.Predicate.isEqual; 32 | import static io.github.glytching.junit.extension.util.ExecutionEvent.Type.FINISHED; 33 | import static io.github.glytching.junit.extension.util.ExecutionEvent.*; 34 | import static org.junit.platform.commons.util.FunctionUtils.where; 35 | 36 | /** 37 | * A {@link EngineExecutionListener} that records all events and makes them available for assertions 38 | * in the extension tests. 39 | * 40 | *

Note: this is mostly lifted from JUnit5's own (unpublished) test utilities. 41 | * 42 | * @see JUnit5 44 | * Test Utilities 45 | */ 46 | public class RecordingExecutionListener implements EngineExecutionListener { 47 | 48 | private final List executionEvents = new CopyOnWriteArrayList<>(); 49 | 50 | // ------------ 51 | // listener callbacks 52 | // ------------ 53 | 54 | @Override 55 | public void dynamicTestRegistered(TestDescriptor testDescriptor) { 56 | addEvent(ExecutionEvent.dynamicTestRegistered(testDescriptor)); 57 | } 58 | 59 | @Override 60 | public void executionSkipped(TestDescriptor testDescriptor, String reason) { 61 | addEvent(ExecutionEvent.executionSkipped(testDescriptor, reason)); 62 | } 63 | 64 | @Override 65 | public void executionStarted(TestDescriptor testDescriptor) { 66 | addEvent(ExecutionEvent.executionStarted(testDescriptor)); 67 | } 68 | 69 | @Override 70 | public void executionFinished(TestDescriptor testDescriptor, TestExecutionResult result) { 71 | addEvent(ExecutionEvent.executionFinished(testDescriptor, result)); 72 | } 73 | 74 | @Override 75 | public void reportingEntryPublished(TestDescriptor testDescriptor, ReportEntry entry) { 76 | addEvent(ExecutionEvent.reportingEntryPublished(testDescriptor, entry)); 77 | } 78 | 79 | // ------------ 80 | // convenience methods for accessing the events received via callbacks, used by test assertions 81 | // ------------ 82 | 83 | public List getExecutionEvents() { 84 | return executionEvents; 85 | } 86 | 87 | public Stream getEventsByType(Type type) { 88 | return eventStream().filter(byType(type)); 89 | } 90 | 91 | public Stream getTestEventsByType(Type type) { 92 | return getEventsByTypeAndTestDescriptor(type, TestDescriptor::isTest); 93 | } 94 | 95 | public Stream getEventsByTypeAndTestDescriptor( 96 | Type type, Predicate predicate) { 97 | return eventStream().filter(byType(type).and(byTestDescriptor(predicate))); 98 | } 99 | 100 | public long countEventsByType(Type type) { 101 | return getEventsByType(type).count(); 102 | } 103 | 104 | public Stream getFinishedEventsByStatus(Status status) { 105 | return getEventsByType(FINISHED) 106 | .filter( 107 | byPayload( 108 | TestExecutionResult.class, where(TestExecutionResult::getStatus, isEqual(status)))); 109 | } 110 | 111 | private Stream eventStream() { 112 | return getExecutionEvents().stream(); 113 | } 114 | 115 | private void addEvent(ExecutionEvent event) { 116 | executionEvents.add(event); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/test/java/io/github/glytching/junit/extension/watcher/WatcherExtensionMetaTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.github.glytching.junit.extension.watcher; 18 | 19 | import org.junit.jupiter.api.BeforeEach; 20 | import org.junit.jupiter.api.Test; 21 | import org.junit.jupiter.api.extension.ExtensionContext; 22 | import org.junit.jupiter.api.extension.ExtensionContext.Namespace; 23 | import org.junit.jupiter.api.extension.ExtensionContext.Store; 24 | import org.mockito.ArgumentCaptor; 25 | import org.mockito.Mock; 26 | import org.mockito.MockitoAnnotations; 27 | 28 | import java.lang.reflect.Method; 29 | import java.util.List; 30 | import java.util.logging.Logger; 31 | 32 | import static org.hamcrest.MatcherAssert.assertThat; 33 | import static org.hamcrest.Matchers.is; 34 | import static org.hamcrest.Matchers.startsWith; 35 | import static org.mockito.Mockito.*; 36 | 37 | /** 38 | * We cannot use the normal test flow to verify the {@link WatcherExtension} because it has 39 | * no easily assertable side effects. So, we test its interaction with the logging subsystem by 40 | * playing around with the {@link WatcherExtension} directly. 41 | */ 42 | public class WatcherExtensionMetaTest { 43 | 44 | @Mock private ExtensionContext extensionContext; 45 | @Mock private Store store; 46 | @Mock private Logger logger; 47 | 48 | private WatcherExtension sut; 49 | 50 | @BeforeEach 51 | public void prepare() { 52 | MockitoAnnotations.initMocks(this); 53 | 54 | when(extensionContext.getStore(Namespace.create(WatcherExtension.class, extensionContext))) 55 | .thenReturn(store); 56 | 57 | sut = new WatcherExtension(logger); 58 | } 59 | 60 | @Test 61 | void willLogBeforeAndAfter() throws Exception { 62 | Method testMethod = 63 | WatcherExtensionTest.class.getMethod("canExecuteATestWithTheWatcherEngaged"); 64 | 65 | givenExtensionContentWithMethod(testMethod); 66 | when(store.remove(eq(testMethod), eq(long.class))).thenReturn(System.currentTimeMillis()); 67 | 68 | sut.beforeTestExecution(extensionContext); 69 | sut.afterTestExecution(extensionContext); 70 | 71 | ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); 72 | verify(logger, times(2)).info(captor.capture()); 73 | 74 | List logEvents = captor.getAllValues(); 75 | assertThat(logEvents.size(), is(2)); 76 | assertThat(logEvents.get(0), is(String.format("Starting test [%s]", testMethod.getName()))); 77 | assertThat( 78 | logEvents.get(1), 79 | startsWith(String.format("Completed test [%s] in ", testMethod.getName()))); 80 | } 81 | 82 | private void givenExtensionContentWithMethod(Method method) { 83 | when(extensionContext.getRequiredTestMethod()).thenReturn(method); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/test/java/io/github/glytching/junit/extension/watcher/WatcherExtensionTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.github.glytching.junit.extension.watcher; 18 | 19 | import org.junit.jupiter.api.Test; 20 | import org.junit.jupiter.api.extension.ExtendWith; 21 | import org.junit.jupiter.api.extension.ExtensionContext; 22 | 23 | /** 24 | * The {@link WatcherExtension} has no easily assertable side effects since all it does it write 25 | * some log events and we cannot easily mock the logger instance it uses when invoking it in the 26 | * normal test flow. 27 | * 28 | *

There seem to be some gaps around testability of JUnit Jupiter extensions e.g. 29 | * 30 | *

    31 | *
  • Cannot inject dependencies into extensions; for the {@link WatcherExtension} it would be 32 | * convenient to be able to inject a mocked Logger instance 33 | *
  • Cannot get our hands on the {@link ExtensionContext} used by a test invocation 34 | *
35 | */ 36 | @ExtendWith(WatcherExtension.class) 37 | public class WatcherExtensionTest { 38 | 39 | @Test 40 | public void canExecuteATestWithTheWatcherEngaged() throws Exception { 41 | // not much more we can do here! 42 | Thread.sleep(20); 43 | } 44 | } 45 | --------------------------------------------------------------------------------