├── .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 |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 | *
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 extends Throwable> 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 | *
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 | *
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 | * 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 Usage examples:
69 | *
70 | * Injecting a {@code TemporaryFolder} in a {@code @BeforeEach} method:
71 | *
72 | * Injecting a {@code TemporaryFolder} in a {@code @Test} method:
103 | *
104 | * Usage example:
25 | *
26 | * Usage examples:
46 | *
47 | * Injecting random values as fields:
48 | *
49 | * Injecting random values as parameters:
86 | *
87 | * 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 Usage example:
28 | *
29 | * Usage example:
29 | *
30 | * System properties can be injected into your test or test case with either of the following
40 | * approaches:
41 | *
42 | * The {@link SystemProperty} annotation is repeatable.
64 | *
65 | * Usage examples:
66 | *
67 | * Declaring system properties at class level:
68 | *
69 | * Declaring system properties at method level:
83 | *
84 | * 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 Usage example:
25 | *
26 | * Usage example:
16 | *
17 | * Injecting random values as fields:
18 | *
19 | * It produces output like so:
39 | *
40 | * Usage example:
46 | *
47 | * 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 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 | * 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 There seem to be some gaps around testability of JUnit Jupiter extensions e.g.
29 | *
30 | *
32 | *
67 | *
68 | *
36 | * private TemporaryFolder temporaryFolder;
37 | *
38 | * @BeforeEach
39 | * public void setUp(TemporaryFolder temporaryFolder) {
40 | * this.temporaryFolder = temporaryFolder
41 | * // ...
42 | * }
43 | *
44 | *
48 | * @Test
49 | * public void testUsingTemporaryFolder(TemporaryFolder temporaryFolder) {
50 | * // ...
51 | * }
52 | *
53 | *
58 | * private static TemporaryFolder TEMPORARY_FOLDER;
59 | *
60 | * @BeforeAll
61 | * public static void setUp(TemporaryFolder givenTemporaryFolder) {
62 | * TEMPORARY_FOLDER = givenTemporaryFolder
63 | * // ...
64 | * }
65 | *
66 | *
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 | *
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 | *
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
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 | *
44 | *
45 | *
50 | * @ExtendWith(RandomBeansExtension.class)
51 | * public class MyTest {
52 | *
53 | * @Random
54 | * private String anyString;
55 | *
56 | * @Random(excluded = {"name", "value"})
57 | * private List
84 | *
85 | *
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
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 | *
32 | */
33 | final class RestoreContext {
34 | private final Set
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 | *
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 | *
38 | *
39 | *
43 | *
62 | *
63 | *
48 | * @SystemProperty(name = "nameA", value = "valueA")
49 | * public class MyTest {
50 | * // ...
51 | * }
52 | *
53 | *
55 | * @Test
56 | * @SystemProperty(name = "nameA", value = "valueA")
57 | * public void testUsingSystemProperty() {
58 | * // ...
59 | * }
60 | *
61 | *
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 | *
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
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 | *
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
33 | *
37 | *
38 | *
41 | * INFO: Starting test: [aTest]
42 | * INFO: Completed test [aTest] in 21 ms.
43 | *
44 | *
45 | *
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
36 | *
44 | *
45 | * But ... some extension behaviours cannot be tested in the normal test execution flow. For
46 | * example:
47 | *
48 | *
49 | *
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 | *
31 | *
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 |
--------------------------------------------------------------------------------