├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── config.yml
│ └── feature_request.md
├── dependabot.yml
├── stale.yml
└── workflows
│ ├── merge-dependabot.yml
│ └── on-push-do-docs.yml
├── .gitignore
├── code_of_conduct.md
├── license.txt
├── readme.md
└── src
├── .editorconfig
├── .gitattributes
├── Directory.Build.props
├── Directory.Packages.props
├── Shared.sln.DotSettings
├── Tests
├── InNamespaceTest.cs
├── MissingRegister.cs
├── NoArgumentsDetectedException.include.md
├── ParametersTests.cs
├── SkipDispose.Dispose_should_flush.verified.txt
├── SkipDispose.Write_after_dispose_should_throw.verified.txt
├── SkipDispose.cs
├── Snippets
│ ├── ClassBeingTested.cs
│ ├── ComplexParameterSample.cs
│ ├── ContextPushedDownSample.cs
│ ├── ContextSample.cs
│ ├── ContextStaticSample.cs
│ ├── CurrentTestSample.cs
│ ├── CustomBase.cs
│ ├── FilterSample.cs
│ ├── FixtureSample.cs
│ ├── ParametersSample.cs
│ ├── TestBaseSample.cs
│ ├── TestExceptionSample.cs
│ ├── UniqueTestNameSample.cs
│ └── XunitLoggerSample.cs
├── StaticConstructor.VerifyLogs.verified.txt
├── StaticConstructor.cs
├── TestContextWithNamespace.cs
├── TestException_Async.cs
├── TestException_Sync.cs
├── Tests.csproj
├── UsingClassFixture.Write_lines.verified.txt
├── UsingClassFixture.cs
├── UsingCurrentException.cs
├── UsingStatic.Async.verified.txt
├── UsingStatic.Null.verified.txt
├── UsingStatic.Overwrites.verified.txt
├── UsingStatic.Split_Lines.verified.txt
├── UsingStatic.Write_lines.verified.txt
├── UsingStatic.cs
├── UsingTestBase.Write_lines.verified.txt
└── UsingTestBase.cs
├── XunitContext.sln
├── XunitContext.sln.DotSettings
├── XunitContext
├── Context.cs
├── Context_CurrentTest.cs
├── Context_Parameters.cs
├── Context_TestName.cs
├── DebugPoker.cs
├── Extensions.cs
├── Filters.cs
├── Fixture
│ ├── ContextFixture.cs
│ └── IContextFixture.cs
├── GlobalUsings.cs
├── Guard.cs
├── ModuleInitializer.cs
├── Parameter.cs
├── SolutionDirectoryFinder.cs
├── TestWriter.cs
├── TraceListener.cs
├── XunitContext.cs
├── XunitContext.csproj
├── XunitContextBase.cs
└── build
│ ├── XunitContext.props
│ └── XunitContext.targets
├── appveyor.yml
├── global.json
├── icon.png
├── key.snk
├── mdsnippets.json
└── nuget.config
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: SimonCropp
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug fix
3 | about: Create a bug fix to help us improve
4 | ---
5 |
6 | Note: New issues raised, where it is clear the submitter has not read the issue template, are likely to be closed with "please read the issue template". Please don't take offense at this. It is simply a time management decision. If someone raises an issue, and can't be bothered to spend the time to read the issue template, then the project maintainers should not be expected to spend the time to read the submitted issue. Often too much time is spent going back and forth in issue comments asking for information that is outlined in the issue template.
7 |
8 |
9 | #### Preamble
10 |
11 | General questions may be better placed [StackOveflow](https://stackoverflow.com/).
12 |
13 | Where relevant, ensure you are using the current stable versions on your development stack. For example:
14 |
15 | * Visual Studio
16 | * [.NET SDK or .NET Core SDK](https://www.microsoft.com/net/download)
17 | * Any related NuGet packages
18 |
19 | Any code or stack traces must be properly formatted with [GitHub markdown](https://guides.github.com/features/mastering-markdown/).
20 |
21 |
22 | #### Describe the bug
23 |
24 | A clear and concise description of what the bug is. Include any relevant version information.
25 |
26 | A clear and concise description of what you expected to happen.
27 |
28 | Add any other context about the problem here.
29 |
30 |
31 | #### Minimal Repro
32 |
33 | Ensure you have replicated the bug in a minimal solution with the fewest moving parts. Often this will help point to the true cause of the problem. Upload this repro as part of the issue, preferably a public GitHub repository or a downloadable zip. The repro will allow the maintainers of this project to smoke test the any fix.
34 |
35 | #### Submit a PR that fixes the bug
36 |
37 | Submit a [Pull Request (PR)](https://help.github.com/articles/about-pull-requests/) that fixes the bug. Include in this PR a test that verifies the fix. If you were not able to fix the bug, a PR that illustrates your partial progress will suffice.
38 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: How to raise feature requests
4 | ---
5 |
6 |
7 | Note: New issues raised, where it is clear the submitter has not read the issue template, are likely to be closed with "please read the issue template". Please don't take offense at this. It is simply a time management decision. If someone raises an issue, and can't be bothered to spend the time to read the issue template, then the project maintainers should not be expected to spend the time to read the submitted issue. Often too much time is spent going back and forth in issue comments asking for information that is outlined in the issue template.
8 |
9 | If you are certain the feature will be accepted, it is better to raise a [Pull Request (PR)](https://help.github.com/articles/about-pull-requests/).
10 |
11 | If you are uncertain if the feature will be accepted, outline the proposal below to confirm it is viable, prior to raising a PR that implements the feature.
12 |
13 | Note that even if the feature is a good idea and viable, it may not be accepted since the ongoing effort in maintaining the feature may outweigh the benefit it delivers.
14 |
15 |
16 | #### Is the feature request related to a problem
17 |
18 | A clear and concise description of what the problem is.
19 |
20 |
21 | #### Describe the solution
22 |
23 | A clear and concise proposal of how you intend to implement the feature.
24 |
25 |
26 | #### Describe alternatives considered
27 |
28 | A clear and concise description of any alternative solutions or features you've considered.
29 |
30 |
31 | #### Additional context
32 |
33 | Add any other context about the feature request here.
34 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: nuget
4 | directory: "/src"
5 | schedule:
6 | interval: daily
7 | open-pull-requests-limit: 10
8 |
--------------------------------------------------------------------------------
/.github/stale.yml:
--------------------------------------------------------------------------------
1 | # Number of days of inactivity before an issue becomes stale
2 | daysUntilStale: 7
3 | # Number of days of inactivity before a stale issue is closed
4 | daysUntilClose: 7
5 | # Set to true to ignore issues in a milestone (defaults to false)
6 | exemptMilestones: true
7 | # Comment to post when marking an issue as stale. Set to `false` to disable
8 | markComment: >
9 | This issue has been automatically marked as stale because it has not had
10 | recent activity. It will be closed if no further activity occurs. Thank you
11 | for your contributions.
12 | # Comment to post when closing a stale issue. Set to `false` to disable
13 | closeComment: false
14 | # Optionally, specify configuration settings that are specific to just 'issues' or 'pulls':
15 | pulls:
16 | daysUntilStale: 30
17 | exemptLabels:
18 | - Question
19 | - Bug
20 | - Feature
21 | - Improvement
--------------------------------------------------------------------------------
/.github/workflows/merge-dependabot.yml:
--------------------------------------------------------------------------------
1 | name: merge-dependabot
2 | on:
3 | pull_request:
4 | jobs:
5 | automerge:
6 | runs-on: ubuntu-latest
7 | if: github.actor == 'dependabot[bot]'
8 | steps:
9 | - name: Dependabot Auto Merge
10 | uses: ahmadnassri/action-dependabot-auto-merge@v2.6.6
11 | with:
12 | target: minor
13 | github-token: ${{ secrets.dependabot }}
14 | command: squash and merge
--------------------------------------------------------------------------------
/.github/workflows/on-push-do-docs.yml:
--------------------------------------------------------------------------------
1 | name: on-push-do-docs
2 | on:
3 | push:
4 | jobs:
5 | release:
6 | runs-on: windows-latest
7 | steps:
8 | - uses: actions/checkout@v4
9 | - name: Run MarkdownSnippets
10 | run: |
11 | dotnet tool install --global MarkdownSnippets.Tool
12 | mdsnippets ${GITHUB_WORKSPACE}
13 | shell: bash
14 | - name: Push changes
15 | run: |
16 | git config --local user.email "action@github.com"
17 | git config --local user.name "GitHub Action"
18 | git commit -m "Docs changes" -a || echo "nothing to commit"
19 | remote="https://${GITHUB_ACTOR}:${{secrets.GITHUB_TOKEN}}@github.com/${GITHUB_REPOSITORY}.git"
20 | branch="${GITHUB_REF:11}"
21 | git push "${remote}" ${branch} || echo "nothing to push"
22 | shell: bash
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.suo
2 | *.user
3 | bin/
4 | obj/
5 | .vs/
6 | *.DotSettings.user
7 | .idea/
8 | *.received.*
9 | nugets/
--------------------------------------------------------------------------------
/code_of_conduct.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at simon.cropp@gmail.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/license.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Simon Cropp
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # XunitContext
2 |
3 | [](https://ci.appveyor.com/project/SimonCropp/XunitContext)
4 | [](https://www.nuget.org/packages/XunitContext/)
5 |
6 | Extends [xUnit](https://xunit.net/) to expose extra context and simplify logging.
7 |
8 | Redirects [Trace.Write](https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.trace.write), [Debug.Write](https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.debug.write), and [Console.Write and Console.Error.Write](https://docs.microsoft.com/en-us/dotnet/api/system.console.write) to [ITestOutputHelper](https://xunit.net/docs/capturing-output). Also provides static access to the current [ITestOutputHelper](https://xunit.net/docs/capturing-output) for use within testing utility methods.
9 |
10 | Uses [AsyncLocal](https://docs.microsoft.com/en-us/dotnet/api/system.threading.asynclocal-1) to track state.
11 |
12 | **See [Milestones](../../milestones?state=closed) for release notes.**
13 |
14 |
15 | ## NuGet package
16 |
17 | https://nuget.org/packages/XunitContext/
18 |
19 |
20 | ## ClassBeingTested
21 |
22 |
23 |
24 | ```cs
25 | static class ClassBeingTested
26 | {
27 | public static void Method()
28 | {
29 | Trace.WriteLine("From Trace");
30 | Console.WriteLine("From Console");
31 | Debug.WriteLine("From Debug");
32 | Console.Error.WriteLine("From Console Error");
33 | }
34 | }
35 | ```
36 | snippet source | anchor
37 |
38 |
39 |
40 | ## XunitContextBase
41 |
42 | `XunitContextBase` is an abstract base class for tests. It exposes logging methods for use from unit tests, and handle the flushing of logs in its `Dispose` method. `XunitContextBase` is actually a thin wrapper over `XunitContext`. `XunitContext`s `Write*` methods can also be use inside a test inheriting from `XunitContextBase`.
43 |
44 |
45 |
46 | ```cs
47 | public class TestBaseSample(ITestOutputHelper output) :
48 | XunitContextBase(output)
49 | {
50 | [Fact]
51 | public void Write_lines()
52 | {
53 | WriteLine("From Test");
54 | ClassBeingTested.Method();
55 |
56 | var logs = XunitContext.Logs;
57 |
58 | Assert.Contains("From Test", logs);
59 | Assert.Contains("From Trace", logs);
60 | Assert.Contains("From Debug", logs);
61 | Assert.Contains("From Console", logs);
62 | Assert.Contains("From Console Error", logs);
63 | }
64 | }
65 | ```
66 | snippet source | anchor
67 |
68 |
69 |
70 | ## xunit Fixture
71 |
72 | In addition to `XunitContextBase` class approach, one is also possible to use `IContextFixture` to gain access to `XunitContext` :
73 |
74 |
75 |
76 | ```cs
77 | public class FixtureSample(ITestOutputHelper helper, ContextFixture ctxFixture) :
78 | IContextFixture
79 | {
80 | Context context = ctxFixture.Start(helper);
81 |
82 | [Fact]
83 | public void Usage()
84 | {
85 | Console.WriteLine("From Test");
86 | Assert.Contains("From Test", context.LogMessages);
87 | }
88 | }
89 | ```
90 | snippet source | anchor
91 |
92 |
93 |
94 | ## Logging
95 |
96 | `XunitContext` provides static access to the logging state for tests. It exposes logging methods for use from unit tests, however registration of [ITestOutputHelper](https://xunit.net/docs/capturing-output) and flushing of logs must be handled explicitly.
97 |
98 |
99 |
100 | ```cs
101 | public class XunitLoggerSample :
102 | IDisposable
103 | {
104 | [Fact]
105 | public void Usage()
106 | {
107 | XunitContext.WriteLine("From Test");
108 |
109 | ClassBeingTested.Method();
110 |
111 | var logs = XunitContext.Logs;
112 |
113 | Assert.Contains("From Test", logs);
114 | Assert.Contains("From Trace", logs);
115 | Assert.Contains("From Debug", logs);
116 | Assert.Contains("From Console", logs);
117 | Assert.Contains("From Console Error", logs);
118 | }
119 |
120 | public XunitLoggerSample(ITestOutputHelper testOutput) =>
121 | XunitContext.Register(testOutput);
122 |
123 | public void Dispose() =>
124 | XunitContext.Flush();
125 | }
126 | ```
127 | snippet source | anchor
128 |
129 |
130 | `XunitContext` redirects [Trace.Write](https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.trace.write), [Console.Write](https://docs.microsoft.com/en-us/dotnet/api/system.console.write), and [Debug.Write](https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.debug.write) in its static constructor.
131 |
132 |
133 |
134 | ```cs
135 | Trace.Listeners.Clear();
136 | Trace.Listeners.Add(new TraceListener());
137 | #if (NETFRAMEWORK)
138 | Debug.Listeners.Clear();
139 | Debug.Listeners.Add(new TraceListener());
140 | #else
141 | DebugPoker.Overwrite(
142 | text =>
143 | {
144 | if (string.IsNullOrEmpty(text))
145 | {
146 | return;
147 | }
148 |
149 | if (text.EndsWith(Environment.NewLine))
150 | {
151 | WriteLine(text.TrimTrailingNewline());
152 | return;
153 | }
154 |
155 | Write(text);
156 | });
157 | #endif
158 | TestWriter writer = new();
159 | Console.SetOut(writer);
160 | Console.SetError(writer);
161 | ```
162 | snippet source | anchor
163 |
164 |
165 | These API calls are then routed to the correct xUnit [ITestOutputHelper](https://xunit.net/docs/capturing-output) via a static [AsyncLocal](https://docs.microsoft.com/en-us/dotnet/api/system.threading.asynclocal-1).
166 |
167 |
168 | ### Logging Libs
169 |
170 | Approaches to routing common logging libraries to Diagnostics.Trace:
171 |
172 | * [Serilog](https://serilog.net/) use [Serilog.Sinks.Trace](https://github.com/serilog/serilog-sinks-trace).
173 | * [NLog](https://github.com/NLog/NLog) use a [Trace target](https://github.com/NLog/NLog/wiki/Trace-target).
174 |
175 |
176 | ## Filters
177 |
178 | `XunitContext.Filters` can be used to filter out unwanted lines:
179 |
180 |
181 |
182 | ```cs
183 | public class FilterSample(ITestOutputHelper output) :
184 | XunitContextBase(output)
185 | {
186 | static FilterSample() =>
187 | Filters.Add(_ => _ != null && !_.Contains("ignored"));
188 |
189 | [Fact]
190 | public void Write_lines()
191 | {
192 | WriteLine("first");
193 | WriteLine("with ignored string");
194 | WriteLine("last");
195 | var logs = XunitContext.Logs;
196 |
197 | Assert.Contains("first", logs);
198 | Assert.DoesNotContain("with ignored string", logs);
199 | Assert.Contains("last", logs);
200 | }
201 | }
202 | ```
203 | snippet source | anchor
204 |
205 |
206 | Filters are static and shared for all tests.
207 |
208 |
209 | ## Context
210 |
211 | For every tests there is a contextual API to perform several operations.
212 |
213 | * `Context.TestOutput`: Access to [ITestOutputHelper](https://xunit.net/docs/capturing-output).
214 | * `Context.Write` and `Context.WriteLine`: Write to the current log.
215 | * `Context.LogMessages`: Access to all log message for the current test.
216 | * [Counters](#counters): Provide access in predicable and incrementing values for the following types: `Guid`, `Int`, `Long`, `UInt`, and `ULong`.
217 | * `Context.Test`: Access to the current `ITest`.
218 | * `Context.SourceFile`: Access to the file path for the current test.
219 | * `Context.SourceDirectory`: Access to the directory path for the current test.
220 | * `Context.SolutionDirectory`: The current solution directory. Obtained by walking up the directory tree from `SourceDirectory`.
221 | * `Context.TestException`: Access to the exception if the current test has failed. See [Test Failure](test-failure).
222 |
223 |
224 |
225 | ```cs
226 | // ReSharper disable UnusedVariable
227 |
228 | public class ContextSample(ITestOutputHelper output) :
229 | XunitContextBase(output)
230 | {
231 | [Fact]
232 | public void Usage()
233 | {
234 | Context.WriteLine("Some message");
235 |
236 | var currentLogMessages = Context.LogMessages;
237 |
238 | var testOutputHelper = Context.TestOutput;
239 |
240 | var currentTest = Context.Test;
241 |
242 | var sourceFile = Context.SourceFile;
243 |
244 | var sourceDirectory = Context.SourceDirectory;
245 |
246 | var solutionDirectory = Context.SolutionDirectory;
247 |
248 | var currentTestException = Context.TestException;
249 | }
250 | }
251 | ```
252 | snippet source | anchor
253 |
254 |
255 | Some members are pushed down to the be accessible directly from `XunitContextBase`:
256 |
257 |
258 |
259 | ```cs
260 | // ReSharper disable UnusedVariable
261 |
262 | public class ContextPushedDownSample(ITestOutputHelper output) :
263 | XunitContextBase(output)
264 | {
265 | [Fact]
266 | public void Usage()
267 | {
268 | WriteLine("Some message");
269 |
270 | var currentLogMessages = Logs;
271 |
272 | var testOutputHelper = Output;
273 |
274 | var sourceFile = SourceFile;
275 |
276 | var sourceDirectory = SourceDirectory;
277 |
278 | var solutionDirectory = SolutionDirectory;
279 |
280 | var currentTestException = TestException;
281 | }
282 | }
283 | ```
284 | snippet source | anchor
285 |
286 |
287 | Context can accessed via a static API:
288 |
289 |
290 |
291 | ```cs
292 | // ReSharper disable UnusedVariable
293 |
294 | public class ContextStaticSample(ITestOutputHelper output) :
295 | XunitContextBase(output)
296 | {
297 | [Fact]
298 | public void StaticUsage()
299 | {
300 | XunitContext.Context.WriteLine("Some message");
301 |
302 | var currentLogMessages = XunitContext.Context.LogMessages;
303 |
304 | var testOutputHelper = XunitContext.Context.TestOutput;
305 |
306 | var currentTest = XunitContext.Context.Test;
307 |
308 | var sourceFile = XunitContext.Context.SourceFile;
309 |
310 | var sourceDirectory = XunitContext.Context.SourceDirectory;
311 |
312 | var solutionDirectory = XunitContext.Context.SolutionDirectory;
313 |
314 | var currentTestException = XunitContext.Context.TestException;
315 | }
316 | }
317 | ```
318 | snippet source | anchor
319 |
320 |
321 |
322 | ### Current Test
323 |
324 | There is currently no API in xUnit to retrieve information on the current test. See issues [#1359](https://github.com/xunit/xunit/issues/1359), [#416](https://github.com/xunit/xunit/issues/416), and [#398](https://github.com/xunit/xunit/issues/398).
325 |
326 | To work around this, this project exposes the current instance of `ITest` via reflection.
327 |
328 | Usage:
329 |
330 |
331 |
332 | ```cs
333 | // ReSharper disable UnusedVariable
334 |
335 | public class CurrentTestSample(ITestOutputHelper output) :
336 | XunitContextBase(output)
337 | {
338 | [Fact]
339 | public void Usage()
340 | {
341 | var currentTest = Context.Test;
342 | // DisplayName will be 'CurrentTestSample.Usage'
343 | var displayName = currentTest.DisplayName;
344 | }
345 |
346 | [Fact]
347 | public void StaticUsage()
348 | {
349 | var currentTest = XunitContext.Context.Test;
350 | // DisplayName will be 'CurrentTestSample.StaticUsage'
351 | var displayName = currentTest.DisplayName;
352 | }
353 | }
354 | ```
355 | snippet source | anchor
356 |
357 |
358 | Implementation:
359 |
360 |
361 |
362 | ```cs
363 | namespace Xunit;
364 |
365 | public partial class Context
366 | {
367 | ITest? test;
368 |
369 |
370 | public ITest Test
371 | {
372 | get
373 | {
374 | InitTest();
375 |
376 | return test!;
377 | }
378 | }
379 |
380 | MethodInfo? methodInfo;
381 |
382 | public MethodInfo MethodInfo
383 | {
384 | get
385 | {
386 | InitTest();
387 | return methodInfo!;
388 | }
389 | }
390 |
391 | Type? testType;
392 |
393 | public Type TestType
394 | {
395 | get
396 | {
397 | InitTest();
398 | return testType!;
399 | }
400 | }
401 |
402 | void InitTest()
403 | {
404 | if (test != null)
405 | {
406 | return;
407 | }
408 |
409 | if (TestOutput == null)
410 | {
411 | throw new(MissingTestOutput);
412 | }
413 |
414 | #if NET8_0_OR_GREATER
415 | [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "test")]
416 | static extern ref ITest GetTest(TestOutputHelper? c);
417 | test = GetTest((TestOutputHelper) TestOutput);
418 | #else
419 | test = (ITest) GetTestMethod(TestOutput)
420 | .GetValue(TestOutput)!;
421 | #endif
422 | var method = (ReflectionMethodInfo) test.TestCase.TestMethod.Method;
423 | var type = (ReflectionTypeInfo) test.TestCase.TestMethod.TestClass.Class;
424 | methodInfo = method.MethodInfo;
425 | testType = type.Type;
426 | }
427 |
428 | public const string MissingTestOutput = "ITestOutputHelper has not been set. It is possible that the call to `XunitContext.Register()` is missing, or the current test does not inherit from `XunitContextBase`.";
429 |
430 | #if !NET8_0_OR_GREATER
431 | static FieldInfo? cachedTestMember;
432 |
433 | static FieldInfo GetTestMethod(ITestOutputHelper testOutput)
434 | {
435 | if (cachedTestMember != null)
436 | {
437 | return cachedTestMember;
438 | }
439 |
440 | var testOutputType = testOutput.GetType();
441 | cachedTestMember = testOutputType.GetField("test", BindingFlags.Instance | BindingFlags.NonPublic);
442 | if (cachedTestMember == null)
443 | {
444 | throw new($"Unable to find 'test' field on {testOutputType.FullName}");
445 | }
446 |
447 | return cachedTestMember;
448 | }
449 | #endif
450 | }
451 | ```
452 | snippet source | anchor
453 |
454 |
455 |
456 | ### Test Failure
457 |
458 | When a test fails it is expressed as an exception. The exception can be viewed by enabling exception capture, and then accessing `Context.TestException`. The `TestException` will be null if the test has passed.
459 |
460 | One common case is to perform some logic, based on the existence of the exception, in the `Dispose` of a test.
461 |
462 |
463 |
464 | ```cs
465 | // ReSharper disable UnusedVariable
466 | public static class GlobalSetup
467 | {
468 | [ModuleInitializer]
469 | public static void Setup() =>
470 | XunitContext.EnableExceptionCapture();
471 | }
472 |
473 | public class TestExceptionSample(ITestOutputHelper output) :
474 | XunitContextBase(output)
475 | {
476 | [Fact(Skip = "Will fail")]
477 | public void Usage() =>
478 | //This tests will fail
479 | Assert.False(true);
480 |
481 | public override void Dispose()
482 | {
483 | var theExceptionThrownByTest = Context.TestException;
484 | var testDisplayName = Context.Test.DisplayName;
485 | var testCase = Context.Test.TestCase;
486 | base.Dispose();
487 | }
488 | }
489 | ```
490 | snippet source | anchor
491 |
492 |
493 |
494 | ### Base Class
495 |
496 | When creating a custom base class for other tests, it is necessary to pass through the source file path to `XunitContextBase` via the constructor.
497 |
498 |
499 |
500 | ```cs
501 | public class CustomBase(
502 | ITestOutputHelper testOutput,
503 | [CallerFilePath] string sourceFile = "")
504 | :
505 | XunitContextBase(testOutput, sourceFile);
506 | ```
507 | snippet source | anchor
508 |
509 |
510 |
511 | ### Parameters
512 |
513 | Provided the parameters passed to the current test when using a `[Theory]`.
514 |
515 | Use cases:
516 |
517 | * To derive the [unique test name](#uniquetestname).
518 | * In extensibility scenarios for example [Verify file naming](https://github.com/SimonCropp/Verify/blob/master/docs/naming.md).
519 |
520 | Usage:
521 |
522 |
523 |
524 | ```cs
525 | public class ParametersSample(ITestOutputHelper output) :
526 | XunitContextBase(output)
527 | {
528 | [Theory]
529 | [MemberData(nameof(GetData))]
530 | public void Usage(string arg)
531 | {
532 | var parameter = Context.Parameters.Single();
533 | var parameterInfo = parameter.Info;
534 | Assert.Equal("arg", parameterInfo.Name);
535 | Assert.Equal(arg, parameter.Value);
536 | }
537 |
538 | public static IEnumerable