Skip to content

Commit 7122522

Browse files
authored
Merge pull request #30 from geeklearningio/develop
Resolve issues 26, 27, 29 + minor improvements
2 parents 2ed58f7 + c1e82dc commit 7122522

File tree

11 files changed

+164
-68
lines changed

11 files changed

+164
-68
lines changed

README.md

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,15 @@ On your .NET Core Unit Test project
3838
* Add your ASP.NET Core web project as a project reference
3939
### Configuration
4040
The Test environment provided by *Testavior* is based on a **Startup Configuration Service** that let you separate the **Production** environment configuration from the **Test** environment configuration.
41-
This configuration service is represented by a contract **IStartupConfigurationService** which define 3 methods: *Configure, ConfigureEnvironment, ConfigureService* that have to be called within the **Startup Routine** to inject environment dependent configuration.
41+
This configuration service is represented by a contract `IStartupConfigurationService` which define 3 methods: `Configure` - `ConfigureEnvironment - ConfigureService` that have to be called within the **Startup Routine** to inject environment dependent configuration.
4242

4343
1 - In your **ASP.NET Core** project:
44-
* Add a *StartupConfigurationService* class (change name if you wish) to your web project.
45-
* Implement the **IStartupConfigurationService** interface (optionally, inherit from *DefaultStartupConfigurationService* to use the default empty implementation)
44+
* Add a `StartupConfigurationService` class (change name if you wish) to your web project.
45+
* Implement the `IStartupConfigurationService` interface (optionally, inherit from `DefaultStartupConfigurationService` to use the default empty implementation)
4646
* Implement the configuration specific to the Production environment and which must not be executed in the Test environment:
47-
* *ConfigureServices*: implement the configuration options that are specific to the Production environment
48-
* *Configure*: implement the *middleware* configuration specific to the Production environment
49-
* *ConfigureEnvironment*: implement what has to be executed before anything
47+
* `ConfigureServices`: implement the configuration options that are specific to the Production environment
48+
* `Configure`: implement the *middleware* configuration specific to the Production environment
49+
* `ConfigureEnvironment`: implement what has to be executed before anything
5050

5151
Sample:
5252
```csharp
@@ -65,19 +65,19 @@ This configuration service is represented by a contract **IStartupConfigurationS
6565
```
6666

6767
2 - In your **Program** class:
68-
Inject your *StartupConfigurationService* by calling the **ConfigureStartup** method on your **WebHostBuilder**:
68+
Inject your `StartupConfigurationService` by calling the `ConfigureStartup` method on your `WebHostBuilder`:
6969
```csharp
7070
new WebHostBuilder()
7171
...
7272
.UseStartup<Startup>()
7373
.ConfigureStartup<StartupConfigurationService>()
7474
```
7575

76-
3 - In your **Startup** class:
77-
* Inject the *IStartupConfigurationService* interface into the Startup class
78-
* Call the *ConfigureEnvironment* method at the end of the Startup constructor
79-
* Call the *ConfigureServices* method at the end of the original Startup ConfigureServices method
80-
* Call the *Configure* method at the beginning of the original Startup Configure method
76+
3 - In your `Startup` class:
77+
* Inject the `IStartupConfigurationService` interface into the `Startup` class
78+
* Call the `ConfigureEnvironment` method at the end of the `Startup` constructor
79+
* Call the `ConfigureServices` method at the end of the original `Startup.ConfigureServices` method
80+
* Call the `Configure` method at the beginning of the original `Startup.Configure` method
8181

8282
Sample:
8383
```csharp
@@ -108,13 +108,38 @@ public class Startup
108108
}
109109
```
110110

111+
4 - In your test project file:
112+
The **Razor** engine uses dependency files (.deps.json) to resolve some references at runtime. So in order to test the **MVC** part of a application, it is necessary to import these files. To do it, add the following section to your `.csproj`:
113+
```xml
114+
<Target Name="CopyDepsFiles" AfterTargets="Build" Condition="'$(TargetFramework)'!=''">
115+
<ItemGroup>
116+
<DepsFilePaths Include="$([System.IO.Path]::ChangeExtension('%(_ResolvedProjectReferencePaths.FullPath)', '.deps.json'))" />
117+
</ItemGroup>
118+
119+
<Copy SourceFiles="%(DepsFilePaths.FullPath)" DestinationFolder="$(OutputPath)" Condition="Exists('%(DepsFilePaths.FullPath)')" />
120+
</Target>
121+
```
122+
123+
5 - For **xUnit** users
124+
If you intend to use xUnit, first follow the [official documention](https://xunit.github.io/docs/getting-started-dotnet-core), then add a `xunit.runner.json` file to your test project:
125+
```json
126+
{
127+
"shadowCopy": false
128+
}
129+
```
130+
and add the following section to your `.csproj`:
131+
```xml
132+
<ItemGroup>
133+
<None Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
134+
</ItemGroup>
135+
```
111136

112137
### Writing Tests
113-
A specific *IStartupConfigurationService* is required for the **Test** environment if you want to implement **Test Specific** configuration.
114-
*Testavior* comes with a test specific *IStartupConfigurationService* implementation: **TestStartupConfigurationService** which provide a **Test Environment** full of useful features (see **Features** section).
115-
Of course you can implement your own *Test StartupConfigurationService* (by using the onboard TestStartupConfigurationService or not).
138+
A specific `IStartupConfigurationService` is required for the **Test** environment if you want to implement **Test Specific** configuration.
139+
*Testavior* comes with a test specific `IStartupConfigurationService` implementation: `TestStartupConfigurationService` which provide a **Test Environment** full of useful features (see **Features** section).
140+
Of course you can implement your own Startup configuration service (by using the onboard `TestStartupConfigurationService` or not).
116141

117-
To create a *Test Environment*, just instanciate the **TestEnvironment** class by passing it your ASP.NET Core application *Startup*, your *IStartupConfigurationService* implementation, the type of your EF Core ObjectContext and the relative path to your ASP.NET Core project (required to resolve MVC views).
142+
To create a *Test Environment*, just instanciate the `TestEnvironment` class by passing it your ASP.NET Core application `Startup`, your `IStartupConfigurationService` implementation, the type of your EF Core ObjectContext and the relative path to your ASP.NET Core project (required to resolve MVC views).
118143
```csharp
119144
var testEnvironment = new TestEnvironment<Startup, TestStartupConfigurationService<[EF_DB_CONTEXT]>>(
120145
Path.Combine(System.AppContext.BaseDirectory, @"[PATH_TO_WEB_APP]"));
@@ -141,7 +166,7 @@ public void ScenarioShouldBeOk()
141166

142167
#### MVC Test
143168
Write a MVC test is almost as easy as testing an API except that you might want to test the **Model** returned by the server and not the **View**.
144-
To do that, *Testavior* provides a **ViewModelRepository** that will intercept and store the view's models returned by the server.
169+
To do that, *Testavior* provides a **ViewModel Repository** that will intercept and store the view's models returned by the server.
145170

146171
You can access to the this repository using the ASP.NET Core dependency injection mechanism:
147172

@@ -154,7 +179,7 @@ public void ScenarioShouldBeOk()
154179

155180
testEnvironment.Client.GetAsync("/").Result.EnsureSuccessStatusCode();
156181

157-
var viewModel = base.TestEnvironment
182+
var viewModel = testEnvironment
158183
.ServiceProvider
159184
.GetRequiredService<ViewModelRepository>()
160185
.Get<[VIEWMODEL_TYPE]>();

sample/Sample.Test.Splecflow/Sample.Test.Splecflow.csproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@
1313
</ItemGroup>
1414

1515
<ItemGroup>
16-
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.0.0" />
17-
<PackageReference Include="MSTest.TestAdapter" Version="1.1.17" />
18-
<PackageReference Include="MSTest.TestFramework" Version="1.1.17" />
19-
<PackageReference Include="SpecFlow" Version="2.1.0" />
16+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.3.0" />
17+
<PackageReference Include="MSTest.TestAdapter" Version="1.1.18" />
18+
<PackageReference Include="MSTest.TestFramework" Version="1.1.18" />
19+
<PackageReference Include="SpecFlow" Version="2.2.0" />
2020
<PackageReference Include="SpecFlow.NetCore" Version="1.0.0-rc8" />
2121
</ItemGroup>
2222

sample/Sample.Test/BaseTestClass.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@
1010
public abstract class BaseTestClass
1111
{
1212
protected TestEnvironment<Startup, TestStartupConfigurationService<BloggingContext>> TestEnvironment { get; } =
13-
new TestEnvironment<Startup, TestStartupConfigurationService<BloggingContext>>(
14-
Path.Combine(System.AppContext.BaseDirectory, @"..\..\..\..\Sample.Web"));
13+
new TestEnvironment<Startup, TestStartupConfigurationService<BloggingContext>>();
1514

1615
protected void CreateBlogs()
1716
{

sample/Sample.Test/Sample.Test.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
</ItemGroup>
1111

1212
<ItemGroup>
13-
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.0.0" />
14-
<PackageReference Include="MSTest.TestAdapter" Version="1.1.17" />
15-
<PackageReference Include="MSTest.TestFramework" Version="1.1.17" />
13+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.3.0" />
14+
<PackageReference Include="MSTest.TestAdapter" Version="1.1.18" />
15+
<PackageReference Include="MSTest.TestFramework" Version="1.1.18" />
1616
</ItemGroup>
1717

1818
<ItemGroup>

sample/Sample.Web/Startup.cs

Lines changed: 41 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,51 +2,56 @@
22
{
33
using GeekLearning.Testavior.Configuration.Startup;
44
using Microsoft.AspNetCore.Authorization;
5-
using Microsoft.AspNetCore.Builder;
6-
using Microsoft.AspNetCore.Hosting;
7-
using Microsoft.AspNetCore.Mvc.Authorization;
8-
using Microsoft.Extensions.DependencyInjection;
9-
using Microsoft.Extensions.Logging;
10-
11-
public class Startup
12-
{
13-
private IStartupConfigurationService externalStartupConfiguration;
14-
15-
public Startup(IHostingEnvironment env, IStartupConfigurationService externalStartupConfiguration = null)
5+
using Microsoft.AspNetCore.Builder;
6+
using Microsoft.AspNetCore.Hosting;
7+
using Microsoft.AspNetCore.Mvc.Authorization;
8+
using Microsoft.Extensions.Configuration;
9+
using Microsoft.Extensions.DependencyInjection;
10+
using Microsoft.Extensions.Logging;
11+
12+
public class Startup
1613
{
17-
this.externalStartupConfiguration = externalStartupConfiguration;
18-
this.externalStartupConfiguration.ConfigureEnvironment(env);
19-
}
14+
private IStartupConfigurationService externalStartupConfiguration;
2015

21-
public void ConfigureServices(IServiceCollection services)
22-
{
23-
services
24-
.AddMvc(c => c.Filters.Add(new AuthorizeFilter(new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build())))
25-
.AddFilterCollection();
26-
27-
// Pass configuration (IConfigurationRoot) to the configuration service if needed
28-
this.externalStartupConfiguration.ConfigureServices(services, null);
29-
}
16+
public Startup(IHostingEnvironment env, IStartupConfigurationService externalStartupConfiguration = null)
17+
{
18+
this.externalStartupConfiguration = externalStartupConfiguration;
19+
this.externalStartupConfiguration.ConfigureEnvironment(env);
20+
21+
var builder = new ConfigurationBuilder()
22+
.SetBasePath(env.ContentRootPath)
23+
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
24+
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
25+
.AddEnvironmentVariables();
26+
Configuration = builder.Build();
27+
}
3028

31-
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
32-
{
33-
this.externalStartupConfiguration.Configure(app, env, loggerFactory);
29+
public IConfigurationRoot Configuration { get; }
3430

35-
using (var serviceScope = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>().CreateScope())
31+
public void ConfigureServices(IServiceCollection services)
3632
{
37-
serviceScope.ServiceProvider.GetService<Data.BloggingContext>().Database.EnsureCreated();
33+
services
34+
.AddMvc(c => c.Filters.Add(new AuthorizeFilter(new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build())))
35+
.AddFilterCollection();
36+
37+
this.externalStartupConfiguration.ConfigureServices(services, Configuration);
3838
}
3939

40-
if (env.IsDevelopment())
40+
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
4141
{
42-
app.UseDeveloperExceptionPage();
43-
}
42+
this.externalStartupConfiguration.Configure(app, env, loggerFactory, Configuration);
4443

45-
app.MapWhen(
46-
httpContext => httpContext.Request.Path.StartsWithSegments("/api"),
47-
apiApp => apiApp.UseMvc());
44+
using (var serviceScope = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>().CreateScope())
45+
{
46+
serviceScope.ServiceProvider.GetService<Data.BloggingContext>().Database.EnsureCreated();
47+
}
4848

49-
app.UseMvc();
49+
if (env.IsDevelopment())
50+
{
51+
app.UseDeveloperExceptionPage();
52+
}
53+
54+
app.UseMvc();
55+
}
5056
}
5157
}
52-
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"Logging": {
3+
"IncludeScopes": false,
4+
"LogLevel": {
5+
"Default": "Debug",
6+
"System": "Information",
7+
"Microsoft": "Information"
8+
}
9+
}
10+
}

sample/Sample.Web/appsettings.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"Logging": {
3+
"IncludeScopes": false,
4+
"LogLevel": {
5+
"Default": "Warning"
6+
}
7+
}
8+
}

src/GeekLearning.Testavior.Configuration/Startup/DefaultStartupConfigurationService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public IServiceProvider ServiceProvider
1717
}
1818
}
1919

20-
public virtual void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { }
20+
public virtual void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IConfigurationRoot configuration) { }
2121

2222
public virtual void ConfigureEnvironment(IHostingEnvironment env) { }
2323

src/GeekLearning.Testavior.Configuration/Startup/IStartupConfigurationService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ public interface IStartupConfigurationService
1111
{
1212
IServiceProvider ServiceProvider { get; }
1313

14-
void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory);
14+
void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IConfigurationRoot configuration);
1515

1616
void ConfigureEnvironment(IHostingEnvironment env);
1717

src/GeekLearning.Testavior/Environment/TestEnvironment.cs

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33
using Configuration.Startup;
44
using Microsoft.AspNetCore.Hosting;
55
using Microsoft.AspNetCore.TestHost;
6+
using Microsoft.Extensions.PlatformAbstractions;
67
using System;
8+
using System.IO;
79
using System.Net.Http;
10+
using System.Reflection;
811

912
public class TestEnvironment<TStartup, TStartupConfigurationService> : ITestEnvironment
1013
where TStartup : class
@@ -24,9 +27,18 @@ public IServiceProvider ServiceProvider
2427
}
2528
}
2629

27-
public TestEnvironment(string contentRootPath = null)
30+
/// <summary>
31+
/// Creates a new instance of the TestEnvironment class.
32+
/// </summary>
33+
/// <param name="targetProjectRelativePath">Optional. Defines the relative path of the target project to test. Can be useful if the automatic detection does not work.</param>
34+
public TestEnvironment(string targetProjectRelativePath = null)
2835
{
29-
this.contentRootPath = contentRootPath;
36+
this.contentRootPath = GetProjectPath(typeof(TStartup).GetTypeInfo().Assembly, targetProjectRelativePath);
37+
if (this.contentRootPath == null)
38+
{
39+
throw new InvalidOperationException("Target project can not be located. Try specify the content root path explicitly.");
40+
}
41+
3042
this.Server = this.CreateTestServer();
3143
this.Client = Server.CreateClient();
3244
}
@@ -40,5 +52,42 @@ protected virtual TestServer CreateTestServer()
4052
.UseStartup<TStartup>()
4153
);
4254
}
55+
56+
/// <summary>
57+
/// Gets the full path to the target project path that we wish to test
58+
/// </summary>
59+
/// <param name="startupAssembly">The target project's assembly.</param>
60+
/// <param name="targetRelativePath">
61+
/// The parent directory of the target project.
62+
/// e.g. src, samples, test, or test/Websites
63+
/// </param>
64+
/// <returns>The full path to the target project.</returns>
65+
private static string GetProjectPath(Assembly startupAssembly, string targetRelativePath = null)
66+
{
67+
// Get name of the target project which we want to test
68+
var projectName = startupAssembly.GetName().Name;
69+
70+
// Get currently executing test project path
71+
var applicationBasePath = PlatformServices.Default.Application.ApplicationBasePath;
72+
73+
// Find the folder which contains the solution file. We then use this information to find the target
74+
// project which we want to test.
75+
var targetDirectoryInfo = new DirectoryInfo(applicationBasePath);
76+
do
77+
{
78+
var testDirectoryInfo =
79+
new DirectoryInfo(Path.Combine(targetDirectoryInfo.FullName, targetRelativePath ?? projectName));
80+
81+
if (testDirectoryInfo.Exists)
82+
{
83+
return Path.GetFullPath(Path.Combine(testDirectoryInfo.FullName));
84+
}
85+
86+
targetDirectoryInfo = targetDirectoryInfo.Parent;
87+
}
88+
while (targetDirectoryInfo.Parent != null);
89+
90+
return null;
91+
}
4392
}
4493
}

src/GeekLearning.Testavior/Environment/TestStartupConfigurationService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public class TestStartupConfigurationService<TDbContext> : IStartupConfiguration
1818
{
1919
public IServiceProvider ServiceProvider { get; private set; }
2020

21-
public virtual void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
21+
public virtual void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IConfigurationRoot configuration)
2222
{
2323
ServiceProvider = app.ApplicationServices;
2424

0 commit comments

Comments
 (0)