diff --git a/.openpublishing.redirection.json b/.openpublishing.redirection.json index 8855dba0a3..21c119698a 100644 --- a/.openpublishing.redirection.json +++ b/.openpublishing.redirection.json @@ -439,6 +439,26 @@ "source_path": "core/saving/explicit-values-generated-properties.md", "redirect_url": "/ef/core/modeling/generated-properties#overriding-values", "redirect_document_id": false + }, + { + "source_path": "core/testing/testing-sample.md", + "redirect_url": "/ef/core/testing/index", + "redirect_document_id": false + }, + { + "source_path": "core/testing/sharing-databases.md", + "redirect_url": "/ef/core/testing/index", + "redirect_document_id": false + }, + { + "source_path": "core/testing/sqlite.md", + "redirect_url": "/ef/core/testing/index", + "redirect_document_id": false + }, + { + "source_path": "core/testing/in-memory.md", + "redirect_url": "/ef/core/testing/index", + "redirect_document_id": false } ] } diff --git a/entity-framework/core/managing-schemas/ensure-created.md b/entity-framework/core/managing-schemas/ensure-created.md index db76519f5a..22090bb518 100644 --- a/entity-framework/core/managing-schemas/ensure-created.md +++ b/entity-framework/core/managing-schemas/ensure-created.md @@ -8,18 +8,18 @@ uid: core/managing-schemas/ensure-created --- # Create and Drop APIs -The EnsureCreated and EnsureDeleted methods provide a lightweight alternative to [Migrations](xref:core/managing-schemas/migrations/index) for managing the database schema. These methods are useful in scenarios when the data is transient and can be dropped when the schema changes. For example during prototyping, in tests, or for local caches. +The and methods provide a lightweight alternative to [Migrations](xref:core/managing-schemas/migrations/index) for managing the database schema. These methods are useful in scenarios when the data is transient and can be dropped when the schema changes. For example during prototyping, in tests, or for local caches. -Some providers (especially non-relational ones) don't support Migrations. For these providers, EnsureCreated is often the easiest way to initialize the database schema. +Some providers (especially non-relational ones) don't support Migrations. For these providers, `EnsureCreated` is often the easiest way to initialize the database schema. > [!WARNING] -> EnsureCreated and Migrations don't work well together. If you're using Migrations, don't use EnsureCreated to initialize the schema. +> `EnsureCreated` and Migrations don't work well together. If you're using Migrations, don't use `EnsureCreated` to initialize the schema. -Transitioning from EnsureCreated to Migrations is not a seamless experience. The simplest way to do it is to drop the database and re-create it using Migrations. If you anticipate using migrations in the future, it's best to just start with Migrations instead of using EnsureCreated. +Transitioning from `EnsureCreated` to Migrations is not a seamless experience. The simplest way to do it is to drop the database and re-create it using Migrations. If you anticipate using migrations in the future, it's best to just start with Migrations instead of using `EnsureCreated`. ## EnsureDeleted -The EnsureDeleted method will drop the database if it exists. If you don't have the appropriate permissions, an exception is thrown. +The `EnsureDeleted` method will drop the database if it exists. If you don't have the appropriate permissions, an exception is thrown. ```csharp // Drop the database if it exists @@ -28,7 +28,7 @@ dbContext.Database.EnsureDeleted(); ## EnsureCreated -EnsureCreated will create the database if it doesn't exist and initialize the database schema. If any tables exist (including tables for another DbContext class), the schema won't be initialized. +`EnsureCreated` will create the database if it doesn't exist and initialize the database schema. If any tables exist (including tables for another `DbContext` class), the schema won't be initialized. ```csharp // Create the database if it doesn't exist @@ -40,7 +40,7 @@ dbContext.Database.EnsureCreated(); ## SQL Script -To get the SQL used by EnsureCreated, you can use the GenerateCreateScript method. +To get the SQL used by `EnsureCreated`, you can use the method. ```csharp var sql = dbContext.Database.GenerateCreateScript(); diff --git a/entity-framework/core/providers/in-memory/index.md b/entity-framework/core/providers/in-memory/index.md index 7a28b907bb..295cf49f2c 100644 --- a/entity-framework/core/providers/in-memory/index.md +++ b/entity-framework/core/providers/in-memory/index.md @@ -7,7 +7,10 @@ uid: core/providers/in-memory/index --- # EF Core In-Memory Database Provider -This database provider allows Entity Framework Core to be used with an in-memory database. The in-memory database can be useful for testing, although the SQLite provider in in-memory mode may be a more appropriate test replacement for relational databases. The in-memory database is designed for testing only. The provider is maintained as part of the [Entity Framework Core Project](https://github.com/dotnet/efcore). +This database provider allows Entity Framework Core to be used with an in-memory database. While some users use the in-memory database for testing, this is generally discouraged; the SQLite provider in in-memory mode is a more appropriate test replacement for relational databases. For more information on how to test EF Core applications, see the [testing documentation](xref:core/testing/index). The provider is maintained as part of the [Entity Framework Core Project](https://github.com/dotnet/efcore). + +> [!WARNING] +> The In-Memory provider was not designed for use outside of testing environments and should never be used as such. ## Install @@ -31,7 +34,7 @@ Install-Package Microsoft.EntityFrameworkCore.InMemory The following resources will help you get started with this provider. -* [Testing with InMemory](xref:core/testing/in-memory) +* [Testing with InMemory](xref:core/testing/testing-without-the-database#inmemory-provider) * [UnicornStore Sample Application Tests](https://github.com/rowanmiller/UnicornStore/blob/master/UnicornStore/src/UnicornStore.Tests/Controllers/ShippingControllerTests.cs) ## Supported Database Engines diff --git a/entity-framework/core/providers/index.md b/entity-framework/core/providers/index.md index ef4a39b09b..d10d89c460 100644 --- a/entity-framework/core/providers/index.md +++ b/entity-framework/core/providers/index.md @@ -22,7 +22,7 @@ Entity Framework Core can access many different databases through plug-in librar |:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------------------------------|:------------------------------------------------------------------------------------------------|:-------------------------------------------|:------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------| | [Microsoft.EntityFrameworkCore.SqlServer](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.SqlServer) | SQL Server 2012 onwards | [EF Core Project](https://github.com/dotnet/efcore/) (Microsoft) | | 6.0 | [docs](xref:core/providers/sql-server/index) | | [Microsoft.EntityFrameworkCore.Sqlite](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.Sqlite) | SQLite 3.7 onwards | [EF Core Project](https://github.com/dotnet/efcore/) (Microsoft) | | 6.0 | [docs](xref:core/providers/sqlite/index) | -| [Microsoft.EntityFrameworkCore.InMemory](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.InMemory) | EF Core in-memory database | [EF Core Project](https://github.com/dotnet/efcore/) (Microsoft) | [Limitations](xref:core/testing/in-memory) | 6.0 | [docs](xref:core/providers/in-memory/index) | +| [Microsoft.EntityFrameworkCore.InMemory](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.InMemory) | EF Core in-memory database | [EF Core Project](https://github.com/dotnet/efcore/) (Microsoft) | [Limitations](xref:core/testing/testing-without-the-database#inmemory-provider) | 6.0 | [docs](xref:core/providers/in-memory/index) | | [Microsoft.EntityFrameworkCore.Cosmos](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.Cosmos) | Azure Cosmos DB SQL API | [EF Core Project](https://github.com/dotnet/efcore/) (Microsoft) | | 6.0 | [docs](xref:core/providers/cosmos/index) | | [Npgsql.EntityFrameworkCore.PostgreSQL](https://www.nuget.org/packages/Npgsql.EntityFrameworkCore.PostgreSQL) | PostgreSQL | [Npgsql Development Team](https://github.com/npgsql) | | 6.0 | [docs](https://www.npgsql.org/efcore/index.html) | | [Pomelo.EntityFrameworkCore.MySql](https://www.nuget.org/packages/Pomelo.EntityFrameworkCore.MySql) | MySQL, MariaDB | [Pomelo Foundation Project](https://github.com/PomeloFoundation) | | 6.0 | [readme](https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql/blob/master/README.md) | diff --git a/entity-framework/core/testing/_static/fake-provider-and-repository-pattern.png b/entity-framework/core/testing/_static/fake-provider-and-repository-pattern.png new file mode 100644 index 0000000000..860e68338e Binary files /dev/null and b/entity-framework/core/testing/_static/fake-provider-and-repository-pattern.png differ diff --git a/entity-framework/core/testing/_static/fake-provider-and-repository-pattern.xml b/entity-framework/core/testing/_static/fake-provider-and-repository-pattern.xml new file mode 100644 index 0000000000..00eb9d506d --- /dev/null +++ b/entity-framework/core/testing/_static/fake-provider-and-repository-pattern.xml @@ -0,0 +1 @@ +7Vpbk6I4FP41Vs0+OAVBQB9b256Zqp7a3vVhtp+2IqQx05HQMba6v34TCWAI3vEy0z7JOQmJnO87lxxoOL3x/AuDyeg7DRFpACucN5z7BgC2Z9viR2oWSmMDL9VEDIdKVygG+D+klJbSTnGIJtpETinhONGVAY1jFHBNBxmjM33aCyX6rgmMkKEYBJCY2h845KNU23atQv8V4WiU7WxbamQMs8lKMRnBkM5WVE6/4fQYpTy9Gs97iEjrZXZJ73tYM5r/MYZivssNP5vWv1+fHyP3DTWbPra+8Ga/qVZ5h2SqHnjw16NQDBB7R0z9cb7IrCGeIZGXwYLgOETMaTjd2QhzNEhgIAdmggZCN+JjIiRbXA7pVMwMH4e5AgavEZPaP6dcLIOUfpJib7vi+gUT0qOEMqEI0QucEi5ncEZfkal/oTGvmJ3ZWy6uHhMxjuZr7WfnqAg+IzpGnC3ElOyGtgJSUdnJ5FnBC0+pRiuUyKZBxcQoX7kAS1wovPbAzjXgQaHgrhIp4yMa0RiSfqHtshQNMW4JqZjzSGmiTPUTcb5QjginnOpwojnm/8jbP7tKel4ZuZ+rlZfCQglr8FmDJoFDRLo5SSrgriRHagxpgc34CoPRKQvQBsMCFWcgixDfMM+p5gtDBHL8rv+P2tEHhufeJQnBgdiaxsuAGCKDIAX89nbP3Q+2alS2glm3lwLb1bzUtk0vtUGFm3qnclPHAKr/IOQeZTd8hGxdGh9vEz7e0ghDkQm9SF7p2bE8mjD6LsoVM2+eHdfak59bSn4Xh61twGamwzi8kyWgLFgInExwoBtaT4bb885euAjDssVKppTic5YdpVDkyqW0WJWeEMPCTIitpNO9s+6p06S3Y5pcw6wV5rgVxMl0O2dTtcMTxeJBcuK2LD3etDolQqaPqe5aLZ/LC7mlhVqlhVI7GAstyZ0/9uF8t82E/03meRFzwmmQJv2r4n9OWbA7Zw91mpNz3dmR696RJWE19xy/xL1yVN2ZxNsWOjWJW3sl2zciEqXY6Fv8HY2ptNBpMm7N1D9PSgbl8+jFU3JW2pVDFEcTjuPoWuOT/4HiU+qAtQcoUEqOwD8wQNkdfSHHPXOA8g0K3xpiawKQC/QA1PIv3BCz22aIuc6O2MFxpIY8VGPUsTs7hh1wbF10HC86hlPfemXSgd1r65Vlh/hbM2YTbvkZNkuSnYvjZjY5r6za++27MZnFt5eAfjW5ztOP8Vt6zPHAgZWiX6oU3XLJeeJKEZhH2d+vH1PN2kOdqU6279p8BO0jK4817Cu9xnAPbSv6vh7Ljer41DT+pc7stR6/D+oAXITDayL2kRxul85vzqFdRd8qHQTP3FUE5iu8v1FCJ5inPUM4lkVdPJwkOYAfus6//DtXUNFm4dOhXIsGrx8QIq/sixeHyLEMiMxkcPruylla83UG9fauQR0cGdSPc0Dz9flH/irFOFFf3v3MTsg95HAIJwIfsan3NpUfjnYf4CsqJLML8sl4ifaHga+wG9dB1OGKqexJa1gpFSQ4imUpKOwsj8ddiQIOILlTA2MchstAUMUaPThomB7o9lKrQgmw6qGG7fufSy9NK7iRHUarTsz1c8PstqzWO9YT5AIN8/x5w3lTkWSfEWchFh9+p2Vw8f280/8f \ No newline at end of file diff --git a/entity-framework/core/testing/choosing-a-testing-strategy.md b/entity-framework/core/testing/choosing-a-testing-strategy.md new file mode 100644 index 0000000000..c2471a943f --- /dev/null +++ b/entity-framework/core/testing/choosing-a-testing-strategy.md @@ -0,0 +1,116 @@ +--- +title: Choosing a testing strategy - EF Core +description: Different approaches to testing applications that use Entity Framework Core +author: roji +ms.date: 11/07/2021 +uid: core/testing/choosing-a-testing-strategy +--- +# Choosing a testing strategy + +As discussed in the [Overview](xref:core/testing/index), a basic decision you need to make is whether your tests will involve your production database system - just as your application does - or whether your tests will run against a [test double](https://martinfowler.com/bliki/TestDouble.html), which replaces your production database system. + +Testing against a real external resource - rather than replacing it with a test double - can involve the following difficulties: + +1. In many cases, it's simply not possible or practical to test against the actual external resource. For example, your application may interact with some service that cannot be easily tested against (because of rate limiting, or the lack of a testing environment). +2. Even when it's possible to involve the real external resource, this may be exceedingly slow: running a large amount of tests against a cloud service may cause tests to take too long. Testing should be part of the developer's everyday workflow, so it's important that tests run quickly. +3. Executing tests against an external resource may involve isolation issues, where tests interfere with one another. For example, multiple tests running in parallel against a database may modify data and cause each other to fail in various ways. Using a test double avoids this, as each test runs against its own, in-memory resource, and is therefore naturally isolated from other tests. + +However, tests which pass against a test double don't guarantee that your program works when running against the real external resource. For example, a database test double may perform case-sensitive string comparisons, whereas the production database system does case-insensitive comparisons. Such issues are only uncovered when tests are executed against your real production database, making these tests an important part of any testing strategy. + +## Testing against the database may be easier than it seems + +Because of the above difficulties with testing against a real database, developers are frequently urged to use test doubles first, and have a robust test suite which they can run frequently on their machines; tests involving the database, in contrast, are supposed to be executed much less frequently, and in many cases also provide far less coverage. We recommend giving more thought to the latter, and suggest that databases may actually be far less affected by the above problems than people tend to think: + +1. Most databases can nowadays be easily installed on the developer's machine. Container-based technologies such as Docker can make this very easy, and technologies such as [Github Workspaces](https://docs.github.com/en/codespaces/overview) and [Dev Container](https://code.visualstudio.com/docs/remote/create-dev-container) set up your entire development environment for you (including the database). When using SQL Server, it's also possible to test against [LocalDB](/sql/database-engine/configure-windows/sql-server-express-localdb) on Windows, or easily set up a Docker image on Linux. +2. Testing against a local database - with a reasonable test dataset - is usually extremely fast: communication is completely local, and test data is typically buffered in memory on the database side. EF Core itself contains over 30,000 tests against SQL Server alone; these complete reliably in a few minutes, execute in CI on every single commit, and are very frequently executed by developers locally. Some developers turn to an in-memory database (a "fake") in the belief that this is needed for speed - this is almost never actually the case. +3. Isolation is indeed a hurdle when running tests against a real database, as tests may modify data and interfere with one another. However, there are various techniques to provide isolation in database testing scenarios; we concentrate on these in [Testing against your production database system](xref:core/testing/testing-with-the-database)). + +The above is not meant to disparage test doubles or to argue against using them. For one thing, test doubles are necessary for some scenarios which cannot be tested otherwise, such as simulating database failure. However, in our experience, users frequently shy away from testing against their database for the above reasons, believing it's slow, hard or unreliable, when that isn't necessarily the case. [Testing against your production database system](xref:core/testing/testing-with-the-database) aims to address this, providing guidelines and samples for writing fast, isolated tests against your database. + +## Different types of test doubles + +[Test doubles](https://martinfowler.com/bliki/TestDouble.html) is a broad term which encompasses very different approaches. This section covers some common techniques involving test doubles for testing EF Core applications: + +1. Use SQLite (in-memory mode) as a database fake, replacing your production database system. +2. Use the EF Core in-memory provider as a database fake, replacing your production database system. +3. Mock or stub out `DbContext` and `DbSet`. +4. Introduce a repository layer between EF Core and your application code, and mock or stub that layer. + +Below, we'll explore what each method means, and compare it with the others. We recommend reading through the different methods to gain a full understanding of each one. If you've decided to write tests which don't involve your production database system, than a repository layer is the only approach allowing the comprehensive and reliable stubbing/mocking of the data layer. However, that approach has a significant cost in terms of implementation and maintenance. + +### SQLite as a database fake + +One possible testing approach is to swap your production database (e.g. SQL Server) with SQLite, effectively using it as a testing "fake". Aside from ease of setup, SQLite has a an [in-memory database](https://sqlite.org/inmemorydb.html) feature which is especially useful for testing: each test is naturally isolated in its own in-memory database, and no actual files need to be managed. + +However, before doing this, it's important to understand that in EF Core, different database providers behave differently - EF Core does not attempt to abstract every aspect of the underlying database system. Fundamentally, this means that testing against SQLite does not guarantee the same results as against SQL Server, or any other database. Here are some examples of possible behavioral differences: + +* The same LINQ query may return different results on different providers. For example, SQL Server does case-insensitive string comparison by default, whereas SQLite is case-sensitive. This can make your tests pass against SQLite where they would fail against SQL Server (or vice versa). +* Some queries which work on SQL Server simply aren't supported on SQLite, because the exact SQL support in these two database differs. +* If your query happens to use a provider-specific method such as SQL Server's [`EF.Functions.DateDiffDay`](xref:core/providers/sql-server/functions#date-and-time-functions), that query will fail on SQLite, and cannot be tested. +* Raw SQL may work, or it may fail or return different results, depending on exactly what is being done. SQL dialects are different in many ways across databases. + +Compared to running tests against your production database system, it's relatively easy to get started with SQLite, and so many users do. Unfortunately, the above limitations tend to eventually become problematic when testing EF Core applications, even if they don't seem to be at the beginning. As a result, we recommend either writing your tests against your real database, or if using a test double is an absolute necessity, taking onboard the cost of a repository pattern as discussed below. + +For information on how to use SQLite for testing, [see this section](xref:core/testing/testing-without-the-database#sqlite-in-memory). + +### In-memory as a database fake + +As an alternative to SQLite, EF Core also comes with an in-memory provider. Although this provider was originally designed to support internal testing of EF Core itself, some developers use it as a database fake when testing EF Core applications. Doing so is **highly discouraged**: as a database fake, in-memory has the same issues as SQLite (see above), but in addition has the following additional limitations: + +* The in-memory provider generally supports less query types than the SQLite provider, since it isn't a relational database. More queries will fail or behave differently in comparison to your production database. +* Transactions are not supported. +* Raw SQL is completely unsupported. Compare this with SQLite, where it's possible to use raw SQL, as long as that SQL works in the same way on SQLite and your production database. +* The in-memory provider has not been optimized for performance, and will generally work slower than SQLite in in-memory mode (or even your production database system). + +In summary, in-memory has all the disadvantages of SQLite, along with a few more - and offers no advantages in return. If you are looking for a simple, in-memory database fake, use SQLite instead of the in-memory provider; but consider using the repository pattern instead as described below. + +For information on how to use in-memory for testing, see the [see this section](xref:core/testing/testing-without-the-database#inmemory). + +### Mocking or stubbing DbContext and DbSet + +This approach typically uses a mock framework to create a test double of `DbContext` and `DbSet`, and tests against those doubles. Mocking `DbContext` can be a good approach for testing various *non-query* functionality, such as calls to or , allowing you to verify that your code called them in write scenarios. + +However, properly mocking `DbSet` *query* functionality is not possible, since queries are expressed via LINQ operators, which are static extension method calls over `IQueryable`. As a result, when some people talk about "mocking `DbSet`", what they really mean is that they create a `DbSet` backed by an in-memory collection, and then evaluate query operators against that collection in memory, just like a simple `IEnumerable`. Rather than a mock, this is actually a sort of fake, where the in-memory collection replaces the the real database. + +Since only the `DbSet` itself is faked and the query is evaluated in-memory, this approach ends up being very similar to using the EF Core in-memory provider: both techniques execute query operators in .NET over an in-memory collection. As a result, this technique suffers from the same drawbacks as well: queries will behave differently (e.g. around case sensitivity) or will simply fail (e.g. because of provider-specific methods), raw SQL won't work and transactions will be ignored at best. As a result, this technique should generally be avoided for testing any query code. + +### Repository pattern + +The approaches above attempted to either swap EF Core's production database provider with a fake testing provider, or to create a `DbSet` backed by an in-memory collection. These techniques are similar in that they evaluate the program's LINQ queries - either in SQLite or in memory - and this is ultimately the source of the difficulties outlined above: a query designed to execute against a specific production database cannot reliably execute elsewhere without issues. + +For a proper, reliable test double, consider introducing a [repository layer](https://martinfowler.com/eaaCatalog/repository.html) which mediates between your application code and EF Core. The production implementation of the repository contains the actual LINQ queries and executes them via EF Core. In testing, the repository abstraction is directly stubbed or mocked without needing any actual LINQ queries, effectively removing EF Core from your testing stack altogether and allowing tests to focus on application code alone. + +The following diagram compares the database fake approach (SQLite/in-memory) with the repository pattern: + +![Comparison of fake provider with repository pattern](_static/fake-provider-and-repository-pattern.png) + +Since LINQ queries are no longer part of testing, you can directly provide query results to your application. Put another way, the previous approaches roughly allow stubbing out *query inputs* (e.g. replacing SQL Server *tables* with an in-memory one), but then still execute the actual query operators in-memory. The repository pattern, in contrast, allows you to stub out *query outputs* directly, allowing for far more powerful and focused unit testing. Note that for this to work, your repository cannot expose any IQueryable-returning methods, as these once again cannot be stubbed out; IEnumerable should be returned instead. + +However, since the repository pattern requires encapsulating each and every (testable) LINQ query in an IEnumerable-returning method, it imposes an additional architectural layer on your application, and can incur significant cost to implement and maintain. This cost should not be discounted when making a choice on how to test an application, especially given that tests against the real database are still likely to be needed for the queries exposed by the repository. + +It's worth noting that repositories do have advantages outside of just testing. They ensure all data access code is concentrated in one place rather than being spread around the application, and if your application needs to support more than one database, then the repository abstraction can be very helpful for tweaking queries across providers. + +For a sample showing testing with a repository, [see this section]](xref:core/testing/testing-without-the-database#repository-pattern). + +## Overall comparison + +The following table provides a quick, comparative view of the different testing techniques, and shows which functionality can be tested under which approach: + +Feature | In-memory | SQLite in-memory | Mock DbContext | Repository pattern | Testing against the database +----------------------------------------- | ------------ | ------------------------- | -------------- | ------------------ | ---------------------------- +Test double type | Fake | Fake | Fake | Mock/stub | Real, no double +Raw SQL? | No | Depends | No | Yes | Yes +Transactions? | No (ignored) | Yes | Yes | Yes | Yes +Provider-specific translations? | No | No | No | Yes | Yes +Exact query behavior? | Depends | Depends | Depends | Yes | Yes +Can use LINQ anywhere in the application? | Yes | Yes | Yes | No* | Yes + +* All testable database LINQ queries must be encapsulated in IEnumerable-returning repository methods, in order to be stubbed/mocked. + +## Summary + +* We recommend that developers have good test coverage of their application running against their actual production database system. This provides confidence that the application actually works in production, and with proper design, tests can execute reliably and quickly. Since these tests are required in any case, it's a good idea to start there, and if needed, add tests using test doubles later, as required. +* If you've decided to use a test double, we recommend implementing the repository pattern, which allows you to stubb or mock out your data access layer above EF Core, rather than using a fake EF Core provider (Sqlite/in-memory) or by mocking `DbSet`. +* If the repository pattern isn't a viable option for some reason, consider using SQLite in-memory databases. +* Avoid the in-memory provider for testing purposes - this is discouraged and only supported for legacy applications. +* Avoid mocking `DbSet` for querying purposes. diff --git a/entity-framework/core/testing/in-memory.md b/entity-framework/core/testing/in-memory.md deleted file mode 100644 index 373035482d..0000000000 --- a/entity-framework/core/testing/in-memory.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: Testing with the EF In-Memory Database - EF Core -description: Using the EF in-memory database to test an Entity Framework Core application -author: ajcvickers -ms.date: 10/27/2016 -uid: core/testing/in-memory ---- - -# Testing with the EF In-Memory Database - -> [!WARNING] -> The EF in-memory database often behaves differently than relational databases. -> Only use the EF in-memory database after fully understanding the issues and trade-offs involved, as discussed in [Testing code that uses EF Core](xref:core/testing/index). - -> [!TIP] -> SQLite is a relational provider and can also use in-memory databases. -> Consider using this for testing to more closely match common relational database behaviors. -> This is covered in [Using SQLite to test an EF Core application](xref:core/testing/sqlite). - -The information on this page now lives in other locations: - -* See [Testing code that uses EF Core](xref:core/testing/index) for general information on testing with the EF in-memory database. -* See [Sample showing how to test applications that use EF Core](xref:core/testing/testing-sample) for a sample using the EF in-memory database. -* See [The EF in-memory database provider](xref:core/providers/in-memory/index) for general information about the EF in-memory database. diff --git a/entity-framework/core/testing/index.md b/entity-framework/core/testing/index.md index 047a743715..65f4bc31fa 100644 --- a/entity-framework/core/testing/index.md +++ b/entity-framework/core/testing/index.md @@ -1,117 +1,26 @@ --- -title: Testing code that uses EF Core - EF Core -description: Different approaches to testing applications that use Entity Framework Core -author: ajcvickers -ms.date: 04/22/2020 +title: Overview of testing applications that use EF Core - EF Core +description: Overview of testing applications that use Entity Framework Core +author: roji +ms.date: 01/17/2021 uid: core/testing/index --- -# Testing code that uses EF Core +# Testing EF Core Applications -Testing code that accesses a database requires either: +Testing is an important concern to almost all application types - it allows you to be sure your application works correctly, and makes it instantly known if its behavior regresses in the future. Since testing may affect how your code is architected, it's highly recommended to plan for a testing early and to ensure good coverage as your application evolves. This introductory section provides a quick overview of various testing strategies for applications using EF Core. -* Running queries and updates against the same database system used in production. -* Running queries and updates against some other easier to manage database system. -* Using test doubles or some other mechanism to avoid using a database at all. +## Involving the database (or not) -This document outlines the trade-offs involved in each of these choices and shows how EF Core can be used with each approach. +When writing tests for your EF Core application, one basic decision you need to make is whether your tests will involve your production database system - just as your application does - or whether your tests will run against a [test double](https://martinfowler.com/bliki/TestDouble.html), which replaces your production database system. Two prominent examples of test doubles in the EF Core context are [SQLite in-memory mode](xref:core/testing/choosing-a-testing-strategy#sqlite-as-a-database-fake), and the [in-memory provider](xref:core/testing/choosing-a-testing-strategy#inmemory-as-a-database-fake). -> [!TIP] -> See [EF Core testing sample](xref:core/testing/testing-sample) for code demonstrating the concepts introduced here. +For an in-depth comparison and analysis of the different approaches, see [Choosing a testing strategy](xref:core/testing/choosing-a-testing-strategy). Below is a short point-by-point summary to help you get up to speed with the different options: -## All database providers are not equal +* Developers frequently avoid testing against their production database system because they believe this is difficult or slow. This isn't always true in our experience, and we suggest giving this approach a chance: [Testing against your production database system](xref:core/testing/testing-with-the-database) provides techniques for doing this reliably and efficiently. Writing at least some tests against your database is usually necessary in any case - to make sure your application actually works against your production database - and tests not involving the database can be limited in what they allow you to test (see below). +* The [in-memory provider](xref:core/testing/choosing-a-testing-strategy#inmemory-as-a-database-fake) will not behave like your real database in many important ways. Some features cannot be tested with it at all (e.g. transactions, raw SQL..), while other features may behave differently than your production database (e.g. case-sensitivity in queries). While in-memory can work for simple, constrained query scenarios, it is highly limited and we discourage its use. + * Mocking `DbSet` for querying is complex and difficult, and suffers from the same disadvantages as the in-memory approach; we discourage this as well. +* [SQLite in-memory mode](xref:core/testing/choosing-a-testing-strategy#sqlite-as-a-database-fake) offers better compatibility with production relational databases, since SQLite is itself a full-fledged relational database. However, there will still be some important discrepancies between SQLite and your production database, and some features cannot be tested at all (e.g. provider-specific methods on EF.Functions). +* For a testing approach that allows you to use a reliable test double for all the functionality of your production database system, it's possible to introduce a [repository layer](xref:core/testing/choosing-a-testing-strategy#repository-pattern) in your application. This allows you to exclude EF Core entirely from testing and to fully mock the repository; however, this alters the architecture of your application in a way which could be significant, and involves more implementation and maintenance costs. -It is very important to understand that EF Core is not designed to abstract every aspect of the underlying database system. -Instead, EF Core is a common set of patterns and concepts that can be used with any database system. -EF Core database providers then layer database-specific behavior and functionality over this common framework. -This allows each database system to do what it does best while still maintaining commonality, where appropriate, with other database systems. +## Further reading -Fundamentally, this means that switching out the database provider will change EF Core behavior and the application can't be expected to function correctly unless it explicitly accounts for any differences in behavior. -That being said, in many cases doing this will work because there is a high degree of commonality amongst relational databases. -This is good and bad. -Good because moving between database systems can be relatively easy. -Bad because it can give a false sense of security if the application is not fully tested against the new database system. - -## Approach 1: Production database system - -As described in the previous section, the only way to be sure you are testing what runs in production is to use the same database system. -For example, if the deployed application uses SQL Azure, then testing should also be done against SQL Azure. - -However, having every developer run tests against SQL Azure while actively working on the code would be both slow and expensive. -This illustrates the main trade-off involved throughout these approaches: when is it appropriate to deviate from the production database system so as to improve test efficiency? - -Luckily, in this case the answer is quite easy: use local or on-premises SQL Server for developer testing. -SQL Azure and SQL Server are extremely similar, so testing against SQL Server is usually a reasonable trade-off. -That being said, it is still wise to run tests against SQL Azure itself before going into production. - -### LocalDB - -All the major database systems have some form of "Developer Edition" for local testing. -SQL Server also has a feature called [LocalDB](/sql/database-engine/configure-windows/sql-server-express-localdb). -The primary advantage of LocalDB is that it spins up the database instance on demand. -This avoids having a database service running on your machine even when you're not running tests. - -LocalDB is not without its issues: - -* It doesn't support everything that [SQL Server Developer Edition](/sql/sql-server/editions-and-components-of-sql-server-version-15?view=sql-server-ver15&preserve-view=true) does. -* It isn't available on Linux. -* It can cause lag on first test run as the service is spun up. - -Personally, I've never found it a problem having a database service running on my dev machine and I would generally recommend using Developer Edition instead. -However, LocalDB may be appropriate for some people, especially on less powerful dev machines. - -[Running SQL Server](/sql/linux/quickstart-install-connect-docker) (or any other database system) in a Docker container (or similar) is another way to avoid running the database system directly on your development machine. - -## Approach 2: SQLite - -EF Core tests the SQL Server provider primarily by running it against a local SQL Server instance. -These tests run tens of thousands of queries in a couple of minutes on a fast machine. -This illustrates that using the real database system can be a performant solution. -It is a myth that using some lighter-weight database is the only way to run tests quickly. - -That being said, what if for whatever reason you can't run tests against something close to your production database system? -The next best choice is to use something with similar functionality. -This usually means another relational database, for which [SQLite](https://sqlite.org/index.html) is the obvious choice. - -SQLite is a good choice because: - -* It runs in-process with your application and so has low overhead. -* It uses simple, automatically created files for databases, and so doesn't require database management. -* It has an in-memory mode that avoids even the file creation. - -However, remember that: - -* SQLite inevitably doesn't support everything that your production database system does. -* SQLite will behave differently than your production database system for some queries. - -So if you do use SQLite for some testing, make sure to also test against your real database system. - -See [Testing with SQLite](xref:core/testing/sqlite) for EF Core specific guidance. - -## Approach 3: The EF Core in-memory database - -EF Core comes with an in-memory database that we use for internal testing of EF Core itself. -This database is in general **not suitable for testing applications that use EF Core**. Specifically: - -* It is not a relational database. -* It doesn't support transactions. -* It cannot run raw SQL queries. -* It is not optimized for performance. - -None of this is very important when testing EF Core internals because we use it specifically where the database is irrelevant to the test. -On the other hand, these things tend to be very important when testing an application that uses EF Core. - -## Unit testing - -Consider testing a piece of business logic that might need to use some data from a database, but is not inherently testing the database interactions. -One option is to use a [test double](https://en.wikipedia.org/wiki/Test_double) such as a mock or fake. - -We use test doubles for internal testing of EF Core. -However, we never try to mock DbContext or IQueryable. -Doing so is difficult, cumbersome, and fragile. -**Don't do it.** - -Instead we use the EF in-memory database when unit testing something that uses DbContext. -In this case using the EF in-memory database is appropriate because the test is not dependent on database behavior. -Just don't do this to test actual database queries or updates. - -The [EF Core testing sample](xref:core/testing/testing-sample) demonstrates tests using the EF in-memory database, as well as SQL Server and SQLite. +For more in-depth information, see [Choosing a testing strategy](xref:core/testing/choosing-a-testing-strategy). For implementation guidelines and code samples, see [Testing against your production database system](xref:core/testing/testing-with-the-database) and [Testing without your production database system](xref:core/testing/testing-without-the-database). diff --git a/entity-framework/core/testing/sharing-databases.md b/entity-framework/core/testing/sharing-databases.md deleted file mode 100644 index 5af9f1c85a..0000000000 --- a/entity-framework/core/testing/sharing-databases.md +++ /dev/null @@ -1,105 +0,0 @@ ---- -title: Sharing databases between tests - EF Core -description: Sample showing how to share a database between multiple tests -author: ajcvickers -ms.date: 04/25/2020 -uid: core/testing/sharing-databases ---- - -# Sharing databases between tests - -The [EF Core testing sample](xref:core/testing/testing-sample) showed how to test applications against different database systems. -For that sample, each test created a new database. -This is a good pattern when using SQLite or the EF in-memory database, but it can involve significant overhead when using other database systems. - -This sample builds on the previous sample by moving database creation into a test fixture. -This allows a single SQL Server database to be created and seeded only once for all tests. - -> [!TIP] -> Make sure to work through the [EF Core testing sample](xref:core/testing/testing-sample) before continuing here. - -It's not difficult to write multiple tests against the same database. -The trick is doing it in a way that the tests don't trip over each other as they run. -This requires understanding: - -* How to safely share objects between tests -* When the test framework runs tests in parallel -* How to keep the database in a clean state for every test - -## The fixture - -We will use a test fixture for sharing objects between tests. -The [XUnit documentation](https://xunit.net/docs/shared-context.html) states that a fixture should be used "when you want to create a single test context and share it among all the tests in the class, and have it cleaned up after all the tests in the class have finished." - -> [!TIP] -> This sample uses [XUnit](https://xunit.net/), but similar concepts exist in other testing frameworks, including [NUnit](https://nunit.org/). - -This means that we need to move database creation and seeding to a fixture class. -Here's what it looks like: - -[!code-csharp[SharedDatabaseFixture](../../../samples/core/Miscellaneous/Testing/ItemsWebApi/SharedDatabaseTests/SharedDatabaseFixture.cs?name=SharedDatabaseFixture)] - -For now, notice how the constructor: - -* Creates a single database connection for the lifetime of the fixture -* Creates and seeds that database by calling the `Seed` method - -Ignore the locking for now; we will come back to it later. - -> [!TIP] -> The creation and seeding code does not need to be async. -> Making it async will complicate the code and will not improve performance or throughput of tests. - -The database is created by first deleting any existing database and then creating a new database. -This ensures that the database matches the current EF model even if it has been changed since the last test run. - -> [!TIP] -> It can be faster to "clean" the existing database using something like [respawn](https://jimmybogard.com/tag/respawn/) rather than re-create it each time. -> However, care must be taken to ensure that the database schema is up-to-date with the EF model when doing this. - -The database connection is disposed when the fixture is disposed. -You may also consider deleting the test database at this point. -However, this will require additional locking and reference counting if the fixture is being shared by multiple test classes. -Also, it is often useful to have the test database still available for debugging failed tests. - -## Using the fixture - -XUnit has a common pattern for associating a test fixture with a class of tests: - -[!code-csharp[UsingTheFixture](../../../samples/core/Miscellaneous/Testing/ItemsWebApi/SharedDatabaseTests/SharedDatabaseTest.cs?name=UsingTheFixture)] - -XUnit will now create a single fixture instance and pass it to each instance of the test class. -(Remember from the first [testing sample](xref:core/testing/testing-sample) that XUnit creates a new test class instance every time it runs a test.) -This means that the database will be created and seeded once and then each test will use this database. - -Note that tests within a single class will not be run in parallel. -This means it is safe for each test to use the same database connection, even though the `DbConnection` object is not thread-safe. - -## Maintaining database state - -Tests often need to mutate the test data with inserts, updates, and deletes. -But these changes will then impact other tests which are expecting a clean, seeded database. - -This can be dealt with by running mutating tests inside a transaction. -For example: - -[!code-csharp[CanAddItem](../../../samples/core/Miscellaneous/Testing/ItemsWebApi/SharedDatabaseTests/SharedDatabaseTest.cs?name=CanAddItem)] - -Notice that the transaction is created as the test starts and disposed when it is finished. -Disposing the transaction causes it to be rolled back, so none of the changes will be seen by other tests. - -The helper method for creating a context (see the fixture code above) accepts this transaction and opts the DbContext into using it. - -## Sharing the fixture - -You may have noticed locking code around database creation and seeding. -This is not needed for this sample since only one class of tests use the fixture, so only a single fixture instance is created. - -However, you may want to use the same fixture with multiple classes of tests. -XUnit will create one fixture instance for each of these classes. -These may be used by different threads running tests in parallel. -Therefore, it is important to have appropriate locking to ensure only one thread does the database creation and seeding. - -> [!TIP] -> A simple `lock` is fine here. -> There is no need to attempt anything more complex, such as any lock-free patterns. diff --git a/entity-framework/core/testing/sqlite.md b/entity-framework/core/testing/sqlite.md deleted file mode 100644 index c2910f7102..0000000000 --- a/entity-framework/core/testing/sqlite.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -title: Testing with SQLite - EF Core -description: Using SQLite to test an Entity Framework Core application -author: ajcvickers -ms.date: 04/24/2020 -uid: core/testing/sqlite ---- - -# Using SQLite to test an EF Core application - -> [!WARNING] -> Using SQLite can be an effective way to test an EF Core application. -> However, problems can arise where SQLite behaves differently from other database systems. -> See [Testing code that uses EF Core](xref:core/testing/index) for a discussion of the issues and trade-offs. - -This document builds uses on the concepts introduced in [Sample showing how to test applications that use EF Core](xref:core/testing/testing-sample). -The code examples shown here come from this sample. - -## Using SQLite in-memory databases - -Normally, SQLite creates databases as simple files and accesses the file in-process with your application. -This is very fast, especially when using a fast [SSD](https://en.wikipedia.org/wiki/Solid-state_drive). - -SQLite can also use databases created purely in-memory. -This is easy to use with EF Core as long as you understand the in-memory database lifetime: - -* The database is created when the connection to it is opened -* The database is deleted when the connection to it is closed - -EF Core will use an already open connection when given one, and will never attempt to close it. -So the key to using EF Core with an in-memory SQLite database is to open the connection before passing it to EF. - -The [sample](xref:core/testing/testing-sample) achieves this with the following code: - -[!code-csharp[SqliteInMemory](../../../samples/core/Miscellaneous/Testing/ItemsWebApi/Tests/SqliteInMemoryItemsControllerTest.cs?name=SqliteInMemory)] - -Notice: - -* The `CreateInMemoryDatabase` method creates a SQLite in-memory database and opens the connection to it. -* The created `DbConnection` is extracted from the `ContextOptions` and saved. -* The connection is disposed when the test is disposed so that resources are not leaked. - -> [!NOTE] -> [Issue #16103](https://github.com/dotnet/efcore/issues/16103) is tracking ways to make this connection management easier. diff --git a/entity-framework/core/testing/testing-sample.md b/entity-framework/core/testing/testing-sample.md deleted file mode 100644 index 42d14ae719..0000000000 --- a/entity-framework/core/testing/testing-sample.md +++ /dev/null @@ -1,226 +0,0 @@ ---- -title: EF Core testing sample - EF Core -description: Sample showing how to test applications which use Entity Framework Core -author: ajcvickers -ms.date: 04/22/2020 -uid: core/testing/testing-sample -no-loc: [Item, Tag, Items, Tags, items, tags] ---- - -# EF Core testing sample - -> [!TIP] -> The code in this document can be found on GitHub as a [runnable sample](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/Testing/ItemsWebApi/). -> Note that some of these tests **are expected to fail**. The reasons for this are explained below. - -This doc walks through a sample for testing code that uses EF Core. - -## The application - -The [sample](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/Testing/ItemsWebApi/) contains two projects: - -- ItemsWebApi: A very simple [Web API backed by ASP.NET Core](/aspnet/core/tutorials/first-web-api) with a single controller -- Tests: An [XUnit](https://xunit.net/) test project to test the controller - -### The model and business rules - -The model backing this API has two entity types: Items and Tags. - -- Items have a case-sensitive name and a collection of Tags. -- Each Tag has a label and a count representing the number of times it has been applied to the Item. -- Each Item should only have one Tag with a given label. - - If an item is tagged with the same label more than once, then the count on the existing tag with that label is incremented instead of a new tag being created. -- Deleting an Item should delete all associated Tags. - -#### The Item entity type - -The `Item` entity type: - -[!code-csharp[ItemEntityType](../../../samples/core/Miscellaneous/Testing/ItemsWebApi/ItemsWebApi/Item.cs?name=ItemEntityType)] - -And its configuration in `DbContext.OnModelCreating`: - -[!code-csharp[ConfigureItem](../../../samples/core/Miscellaneous/Testing/ItemsWebApi/ItemsWebApi/ItemsContext.cs?name=ConfigureItem)] - -Notice that entity type constrains the way it can be used to reflect the domain model and business rules. In particular: - -- The primary key is mapped directly to the `_id` field and not exposed publicly - - EF detects and uses the private constructor accepting the primary key value and name. -- The `Name` property is read-only and set only in the constructor. -- Tags are exposed as a `IReadOnlyList` to prevent arbitrary modification. - - EF associates the `Tags` property with the `_tags` backing field by matching their names. - - The `AddTag` method takes a tag label and implements the business rule described above. - That is, a tag is only added for new labels. - Otherwise the count on an existing label is incremented. -- The `Tags` navigation property is configured for a many-to-one relationship - - There is no need for a navigation property from Tag to Item, so it is not included. - - Also, Tag does not define a foreign key property. - Instead, EF will create and manage a property in shadow-state. - -#### The Tag entity type - -The `Tag` entity type: - -[!code-csharp[TagEntityType](../../../samples/core/Miscellaneous/Testing/ItemsWebApi/ItemsWebApi/Tag.cs?name=TagEntityType)] - -And its configuration in `DbContext.OnModelCreating`: - -[!code-csharp[ConfigureTag](../../../samples/core/Miscellaneous/Testing/ItemsWebApi/ItemsWebApi/ItemsContext.cs?name=ConfigureTag)] - -Similarly to Item, Tag hides its primary key and makes the `Label` property read-only. - -### The ItemsController - -The Web API controller is pretty basic. -It gets a `DbContext` from the dependency injection container through constructor injection: - -[!code-csharp[Constructor](../../../samples/core/Miscellaneous/Testing/ItemsWebApi/ItemsWebApi/Controllers/ItemsController.cs?name=Constructor)] - -It has methods to get all Items or an Item with a given name: - -[!code-csharp[Get](../../../samples/core/Miscellaneous/Testing/ItemsWebApi/ItemsWebApi/Controllers/ItemsController.cs?name=Get)] - -It has a method to add a new Item: - -[!code-csharp[PostItem](../../../samples/core/Miscellaneous/Testing/ItemsWebApi/ItemsWebApi/Controllers/ItemsController.cs?name=PostItem)] - -A method to tag an Item with a label: - -[!code-csharp[PostTag](../../../samples/core/Miscellaneous/Testing/ItemsWebApi/ItemsWebApi/Controllers/ItemsController.cs?name=PostTag)] - -And a method to delete an Item and all associated Tags: - -[!code-csharp[DeleteItem](../../../samples/core/Miscellaneous/Testing/ItemsWebApi/ItemsWebApi/Controllers/ItemsController.cs?name=DeleteItem)] - -Most validation and error handling have been removed to reduce clutter. - -## The Tests - -The tests are organized to run with multiple database provider configurations: - -- The SQL Server provider, which is the provider used by the application -- The SQLite provider -- The SQLite provider using in-memory SQLite databases -- The EF in-memory database provider - -This is achieved by putting all the tests in a base class, then inheriting from this to test with each provider. - -> [!TIP] -> You will need to change the SQL Server connection string if you're not using LocalDB. -> See [Testing with SQLite](xref:core/testing/sqlite) for guidance on using SQLite for in-memory testing. - -The following two tests are expected to fail: - -- `Can_remove_item_and_all_associated_tags` when running with the EF in-memory database provider -- `Can_add_item_differing_only_by_case` when running with the SQL Server provider - -This is covered in more detail below. - -### Setting up and seeding the database - -XUnit, like most testing frameworks, will create a new test class instance for each test run. -Also, XUnit will not run tests within a given test class in parallel. -This means that we can set up and configure the database in the test constructor and it will be in a well-known state for each test. - -> [!TIP] -> This sample recreates the database for each test. -> This works well for SQLite and EF in-memory database testing but can involve significant overhead with other database systems, including SQL Server. -> Approaches for reducing this overhead are covered in [Sharing databases across tests](xref:core/testing/sharing-databases). - -When each test is run: - -- DbContextOptions are configured for the provider in use and passed to the base class constructor - - These options are stored in a property and used throughout the tests for creating DbContext instances -- A Seed method is called to create and seed the database - - The Seed method ensures the database is clean by deleting it and then re-creating it - - Some well-known test entities are created and saved to the database - -[!code-csharp[Seeding](../../../samples/core/Miscellaneous/Testing/ItemsWebApi/Tests/ItemsControllerTest.cs?name=Seeding)] - -Each concrete test class then inherits from this. -For example: - -[!code-csharp[SqliteItemsControllerTest](../../../samples/core/Miscellaneous/Testing/ItemsWebApi/Tests/SqliteItemsControllerTest.cs?name=SqliteItemsControllerTest)] - -### Test structure - -Even though the application uses dependency injection, the tests do not. -It would be fine to use dependency injection here, but the additional code it requires has little value. -Instead, a DbContext is created using `new` and then directly passed as the dependency to the controller. - -Each test then executes the method under test on the controller and asserts the results are as expected. -For example: - -[!code-csharp[CanGetItems](../../../samples/core/Miscellaneous/Testing/ItemsWebApi/Tests/ItemsControllerTest.cs?name=CanGetItems)] - -Notice that different DbContext instances are used to seed the database and run the tests. -This ensures that the test is not using (or tripping over) entities tracked by the context when seeding. -It also better matches what happens in web apps and services. - -Tests that mutate the database create a second DbContext instance in the test for similar reasons. -That is, creating a new, clean, context and then reading into it from the database to ensure that the changes were saved to the database. -For example: - -[!code-csharp[CanAddItem](../../../samples/core/Miscellaneous/Testing/ItemsWebApi/Tests/ItemsControllerTest.cs?name=CanAddItem)] - -Two slightly more involved tests cover the business logic around adding tags. - -[!code-csharp[CanAddTag](../../../samples/core/Miscellaneous/Testing/ItemsWebApi/Tests/ItemsControllerTest.cs?name=CanAddTag)] - -[!code-csharp[CanUpTagCount](../../../samples/core/Miscellaneous/Testing/ItemsWebApi/Tests/ItemsControllerTest.cs?name=CanUpTagCount)] - -## Issues using different database providers - -Testing with a different database system than is used in the production application can lead to problems. -These are covered at the conceptual level in [Testing code that uses EF Core](xref:core/testing/index). -The sections below cover two examples of such issues demonstrated by the tests in this sample. - -### Test passes when the application is broken - -One of the requirements for our application is that "Items have a case-sensitive name and a collection of Tags." -This is pretty simple to test: - -[!code-csharp[CanAddItemCaseInsensitive](../../../samples/core/Miscellaneous/Testing/ItemsWebApi/Tests/ItemsControllerTest.cs?name=CanAddItemCaseInsensitive)] - -Running this test against the EF in-memory database indicates that everything is fine. -Everything still looks fine when using SQLite. -But the test fails when run against SQL Server! - -```output -System.InvalidOperationException : Sequence contains more than one element - at System.Linq.ThrowHelper.ThrowMoreThanOneElementException() - at System.Linq.Enumerable.Single[TSource](IEnumerable`1 source) - at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.Execute[TResult](Expression query) - at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.Execute[TResult](Expression expression) - at System.Linq.Queryable.Single[TSource](IQueryable`1 source, Expression`1 predicate) - at Tests.ItemsControllerTest.Can_add_item_differing_only_by_case() -``` - -This is because both the EF in-memory database and the SQLite database are case-sensitive by default. -SQL Server, on the other hand, is case-insensitive! - -EF Core, by design, does not change these behaviors because forcing a change in case-sensitivity can have a big performance impact. - -Once we know this is a problem we can fix the application and compensate in tests. -However, the point here is that this bug could be missed if only testing with the EF in-memory database or SQLite providers. - -### Test fails when the application is correct - -Another of the requirements for our application is that "deleting an Item should delete all associated Tags." -Again, easy to test: - -[!code-csharp[DeleteItem](../../../samples/core/Miscellaneous/Testing/ItemsWebApi/Tests/ItemsControllerTest.cs?name=DeleteItem)] - -This test passes on SQL Server and SQLite, but fails with the EF in-memory database! - -```output -Assert.False() Failure -Expected: False -Actual: True - at Tests.ItemsControllerTest.Can_remove_item_and_all_associated_tags() -``` - -In this case, the application is working correctly because SQL Server supports [cascade deletes](xref:core/saving/cascade-delete). -SQLite also supports cascade deletes, as do most relational databases, so testing this on SQLite works. -On the other hand, the EF in-memory database [does not support cascade deletes](https://github.com/dotnet/efcore/issues/3924). -This means that this part of the application cannot be tested with the EF in-memory database provider. diff --git a/entity-framework/core/testing/testing-with-the-database.md b/entity-framework/core/testing/testing-with-the-database.md new file mode 100644 index 0000000000..8e6d8225e1 --- /dev/null +++ b/entity-framework/core/testing/testing-with-the-database.md @@ -0,0 +1,134 @@ +--- +title: Testing against your Production Database System - EF Core +description: Techniques for testing EF Core applications against your production database system +author: roji +ms.date: 1/24/2022 +uid: core/testing/testing-with-the-database +--- +# Testing against your production database system + +In this page, we discuss techniques for writing automated tests which involve the database system against which the application runs in production. Alternate testing approaches exist, where the production database system is swapped out by test doubles; see the [testing overview page](xref:core/testing/index) for more information. Note that testing against an a different database than what is used in production (e.g. Sqlite) is not covered here, since the different database is used as a test double; this approach is covered in [Testing without your production database system](xref:core/testing/testing-without-the-database). + +The main hurdle with testing which involves a real database is to ensure proper test isolation, so that tests running in parallel (or even in serial) don't interfere with each other. The full sample code for the below can be viewed [here](https://github.com/dotnet/EntityFramework.Docs/blob/main/samples/core/Testing/TestingWithTheDatabase). + +> [!TIP] +> This page shows [xUnit](https://xunit.net/) techniques, but similar concepts exist in other testing frameworks, including [NUnit](https://nunit.org/). + +## Setting up your database system + +Most database systems nowadays can be easily installed, both in CI environments and on developer machines. While it's frequently easy enough to install the database via the regular installation mechanism, ready-to-use Docker images are available for most major databases and can make installation particularly easy in CI. For the developer environment, [Github Workspaces](https://docs.github.com/en/codespaces/overview), [Dev Container](https://code.visualstudio.com/docs/remote/create-dev-container) can set up all needed services and dependencies - including the database. While this requires an initial investment in setup, once that's done you have a working testing environment and can concentrate on more important things. + +In certain cases, databases have a special edition or version which can be helpful for testing. When using SQL Server, [LocalDB](/sql/database-engine/configure-windows/sql-server-express-localdb) can be used to run tests locally with virtually no setup at all, spinning up the database instance on demand and possibly saving resources on less powerful developer machines. However, LocalDB is not without its issues: + +* It doesn't support everything that [SQL Server Developer Edition](/sql/sql-server/editions-and-components-of-sql-server-version-15#-editions) does. +* It's only available on Windows. +* It can cause lag on first test run as the service is spun up. + +We generally recommend installing SQL Server Developer edition rather than LocalDB, since it provides the full SQL Server feature set and is generally very easy to do. + +When using a cloud database, it's usually appropriate to test against a local version of the database, both to improve speed and to decrease costs. For example, when using SQL Azure in production, you can test against a locally-installed SQL Server - the two are extremely similar (though it's still wise to run tests against SQL Azure itself before going into production). When using Cosmos, [the Cosmos emulator](/azure/cosmos-db/local-emulator) is a useful tool both for developing locally and for running tests. + +## Creating, seeding and managing a test database + +Once your database is installed, you're ready to start using it in your tests. In most simple cases, your test suite has a single database that's shared between multiple tests across multiple test classes, so we need some logic to make sure the database is created and seeded exactly once during the lifetime of the test run. + +When using Xunit, this can be done via a [class fixture](https://xunit.net/docs/shared-context#class-fixture), which represents the database and is shared across multiple test runs: + +[!code-csharp[Main](../../../samples/core/Testing/TestingWithTheDatabase/TestDatabaseFixture.cs?name=TestDatabaseFixture)] + +When the above fixture is instantiated, it uses to drop the database (in case it exists from a previous run), and then to create it with your latest model configuration ([see the docs for these APIs](xref:core/managing-schemas/ensure-created)). Once the database is created, the fixture seeds it with some data our tests can use. It's worth spending some time thinking about your seed data, since changing it later for a new test may cause existing tests to fail. + +To use the fixture in a test class, simply implement `IClassFixture` over your fixture type, and xUnit will inject it into your constructor: + +[!code-csharp[Main](../../../samples/core/Testing/TestingWithTheDatabase/BloggingControllerTest.cs?name=UsingTheFixture)] + +Your test class now has a `Fixture` property which can be used by tests to create a fully functional context instance: + +[!code-csharp[Main](../../../samples/core/Testing/TestingWithTheDatabase/BloggingControllerTest.cs?name=GetBlog&highlight=4)] + +Finally, you may have noticed some locking in the fixture's creation logic above. If the fixture is only used in a single test class, it is guaranteed to be instantiated exactly once by xUnit; but it's common to use the same database fixture in multiple test classes. xUnit does provide [collection fixtures](https://xunit.net/docs/shared-context#collection-fixture), but that mechanism prevents your test classes from running in parallel, which is important for test performance. To safely manage this with an xUnit class fixture, we take a simple lock around database creation and seeding, and use a static flag to make sure we never to do it twice. + +## Tests which modify data + +The above example showed a read-only test, which is the easy case from a test isolation standpoint: since nothing is being modified, test interference isn't possible. In contrast, tests which modify data are more problematic, since they may interfere with one another. One common technique to isolate writing tests is to wrap the test in a transaction, and to have that transaction rolled back at the end of the test. Since nothing is actually committed to the database, other tests don't see any modifications and interference is avoided. + +Here's a controller method which adds a Blog to our database: + +[!code-csharp[Main](../../../samples/core/Testing/BloggingWebApi/Controllers/BloggingController.cs?name=PostBlog)] + +We can test this method with the following: + +[!code-csharp[Main](../../../samples/core/Testing/TestingWithTheDatabase/BloggingControllerTest.cs?name=AddBlog&highlight=5,10)] + +Some notes on the test code above: + +* We start a transaction to make sure the changes below aren't committed to the database, and don't interfere with other tests. Since the transaction is never committed, it is implicitly rolled back at the end of the test when the context instance is disposed. +* After making the updates we want, we clear the context instance's change tracker with , to make sure we actually load the blog from the database below. We could use two context instances instead, but we'd then have to make sure the same transaction is used by both instances. +* You may even want to start the transaction in the fixture's `CreateContext`, so that tests receive a context instance that's already in a transaction, and ready for updates. This can help prevent cases where the transaction is accidentally forgotten, leading to test interference which can be hard to debug. You may also want to separate read-only and write tests in different test classes as well. + +## Tests which explicitly manage transactions + +There is one final category of tests which presents an additional difficulty: tests which modify data and also explicitly manage transactions. Because databases do not typically support nested transactions, it isn't possible to use transactions for isolation as above, since they need to be used by actual product code. While these tests tend to be more rare, it's necessary to handle them in a special way: you must clean up your database to its original state after each test, and parallelization must be disabled so that these tests don't interfere with each other. + +Let's examine the following controller method as an example: + +[!code-csharp[Main](../../../samples/core/Testing/BloggingWebApi/Controllers/BloggingController.cs?name=UpdateBlogUrl&highlight=5)] + +Let's assume that for some reason, the method requires a serializable transaction to be used (this isn't typically the case). As a result, we cannot use a transaction to guarantee test isolation. Since the test will actually commit changes to the database, we'll define another fixture with its own, separate database, to make sure we don't interfere with the other tests already shown above: + +[!code-csharp[Main](../../../samples/core/Testing/TestingWithTheDatabase/TransactionalTestDatabaseFixture.cs?name=TransactionalTestDatabaseFixture)] + +This fixture is similar to the one used above, but notably contains a `Cleanup` method; we'll call this after every test to ensure that the database is reset to its starting state. + +If this fixture will only be used by a single test class, we can reference it as a class fixture as above - xUnit doesn't parallelize tests within the same class (read more about test collections and parallelization in the [xUnit docs](https://xunit.net/docs/running-tests-in-parallel.html)). If, however, we want to share this fixture between multiple classes, we must make sure these classes don't run in parallel, to avoid any interference. To do that, we will use this as an xUnit [collection fixture](https://xunit.net/docs/shared-context#collection-fixture) rather than as a [class fixture](https://xunit.net/docs/shared-context#class-fixture). + +First, we define a *test collection*, which references our fixture and will be used by all transactional test classes which require it: + +[!code-csharp[Main](../../../samples/core/Testing/TestingWithTheDatabase/TransactionalTestDatabaseFixture.cs?name=CollectionDefinition)] + +We now reference the test collection in our test class, and accept the fixture in the constructor as before: + +[!code-csharp[Main](../../../samples/core/Testing/TestingWithTheDatabase/TransactionalBloggingControllerTest.cs?name=UsingTheFixture&highlight=1,4)] + +Finally, we make our test class disposable, arranging for the fixture's `Cleanup` method to be called after each test: + +[!code-csharp[Main](../../../samples/core/Testing/TestingWithTheDatabase/TransactionalBloggingControllerTest.cs?name=Dispose)] + +Note that since xUnit only ever instantiates the collection fixture once, there is no need for us to use locking around database creation and seeding as we did above. + +The full sample code for the above can be viewed [here](https://github.com/dotnet/EntityFramework.Docs/blob/main/samples/core/Testing/IntegrationTests/TransactionalBloggingControllerTest.cs). + +> [!TIP] +> If you have multiple test classes with tests which modify the database, you can still run them in parallel by having different fixtures, each referencing its own database. Creating and using many test databases isn't problematic and should be done whenever it's helpful. + +## Efficient database creation + +In the samples above, we used and before running tests, to make sure we have an up-to-date test database. These operations can be a bit slow in certain databases, which can be a problem as you iterate over code changes and re-run tests over and over. If that's the case, you may want to temporarily comment out `EnsureDeleted` in your fixture's constructor: this will reuse the same database across test runs. + +The disadvantage of this approach is that if you change your EF Core model, your database schema won't be up to date, and tests may fail. As a result, we only recommend doing this temporarily during the development cycle. + +## Efficient database cleanup + +We saw above that when changes are actually committed to the database, we must clean up the database between every test to avoid interference. In the transactional test sample above, we did this by using EF Core APIs to delete the table's contents: + +[!code-csharp[Main](../../../samples/core/Testing/TestingWithTheDatabase/TransactionalTestDatabaseFixture.cs?name=Cleanup)] + +This typically isn't the most efficient way to clear out a table. If test speed is a concern, you may want to use raw SQL to delete the table instead: + +```sql +DELETE FROM [Blogs] WHERE 1=1; +``` + +You may also want to consider using the [respawn](https://github.com/jbogard/respawn) package, which efficiently clears out a database. In addition, it does not require you to specify the tables to be cleared, and so your cleanup code does not need to be updated as tables are added to your model. + +## Summary + +* When test against a real database, it's worth distinguishing between the following test categories: + * Read-only tests are relatively simple, and can always execute in parallel against the same database without having to worry about isolation. + * Write tests are more problematic, but transactions can be used to make sure they're properly isolated. + * Transactional tests are the most problematic, requiring logic to reset the database back to its original state, as well as disabling parallelization. +* Separating these test categories out into separate classes may avoid confusion and accidental interference between tests. +* Give some thought up-front to your seeded test data, and try to write your tests in a way that won't break too often if that seed data changes. +* Use multiple databases to parallelize tests which modify the database, and possibly also to allow different seed data configurations. +* If test speed is a concern, you may want to look at more efficient techniques for creating your test database, and for cleaning its data between runs. +* Always keep test parallelization and isolation in mind. diff --git a/entity-framework/core/testing/testing-without-the-database.md b/entity-framework/core/testing/testing-without-the-database.md new file mode 100644 index 0000000000..18aa014b5f --- /dev/null +++ b/entity-framework/core/testing/testing-without-the-database.md @@ -0,0 +1,63 @@ +--- +title: Testing without your Production Database System - EF Core +description: Techniques for testing EF Core applications without involving your production database system +author: roji +ms.date: 01/24/2022 +uid: core/testing/testing-without-the-database +--- +# Testing without your production database system + +In this page, we discuss techniques for writing automated tests which do not involve the database system against which the application runs in production, by swapping your database with a [test double](https://en.wikipedia.org/wiki/Test_double). There are various types of test doubles and approaches for doing this, and it's recommended to thoroughly read [Choosing a testing strategy](xref:core/testing/choosing-a-testing-strategy) to fully understand the different options. Finally, it's also possible to test against your production database system; this is covered in [Testing against your production database system](xref:core/testing/testing-with-the-database). + +> [!TIP] +> This page shows [xUnit](https://xunit.net/) techniques, but similar concepts exist in other testing frameworks, including [NUnit](https://nunit.org/). + +## Repository pattern + +If you've decided to write tests without involving your production database system, then the recommended technique for doing so is the repository pattern; for more background on this, see [this section](xref:core/testing/choosing-a-testing-strategy#repository-pattern). The first step of implementing the repository pattern is to extract out your EF Core LINQ queries to a separate layer, which we'll later stub or mock. Here's an example of a repository interface for our blogging system: + +[!code-csharp[Main](../../../samples/core/Testing/BusinessLogic/IBloggingRepository.cs?name=IBloggingRepository)] + +... and here's a partial sample implementation for production use: + +[!code-csharp[Main](../../../samples/core/Testing/BusinessLogic/BloggingRepository.cs?name=BloggingRepository)] + +There's not much to it: the repository simply wraps an EF Core context, and exposes methods which execute the database queries and updates on it. A key point to note is that our `GetAllBlogs` method returns `IEnumerable`, and not `IQueryable`. Returning the latter would mean that query operators can still be composed over the result, requiring that EF Core still be involved in translating the query; this would defeat the purpose of having a repository in the first place. `IEnumerable` allows us to easily stub or mock what the repository returns. + +For an ASP.NET application, we need to register the repository as a service in dependency injection by adding the following to the application's `ConfigureServices`: + +[!code-csharp[Main](../../../samples/core/Testing/BloggingWebApi/Startup.cs?name=RegisterRepositoryInDI)] + +Finally, our ASP.NET controllers get injected with the repository service instead of the EF Core context, and execute methods on it: + +[!code-csharp[Main](../../../samples/core/Testing/BloggingWebApi/Controllers/BloggingControllerWithRepository.cs?name=BloggingControllerWithRepository&highlight=8)] + +At this point, your application is architected according to the repository pattern: the only point of contact with the data access layer - EF Core - is now via the repository layer, which acts as a mediator between application code and actual database queries. Tests can now be written simply by stubbing out the repository, or by mocking it with your favorite mocking library. Here's an example of a mock-based test using the popular [Moq](https://github.com/Moq/moq4) library: + +[!code-csharp[Main](../../../samples/core/Testing/TestingWithoutTheDatabase/RepositoryBloggingControllerTest.cs?name=GetBlog)] + +The full sample code can be viewed [here](https://github.com/dotnet/EntityFramework.Docs/blob/main/samples/core/Testing/TestingWithoutTheDatabase/RepositoryBloggingControllerTest.cs). + +## SQLite in-memory + +SQLite can easily be configured as the EF Core provider for your test suite instead of your production database system (e.g. SQL Server); consult the [SQLite provider docs](xref:core/providers/sqlite/index) for details. However, it's usually a good idea to use SQLite's [in-memory database](https://sqlite.org/inmemorydb.html) feature when testing, since it provides easy isolation between tests, and does not require dealing with actual SQLite files. + +To use in-memory SQLite, it's important to understand that a new database is created whenever a low-level connection is opened, and that it's deleted what that connection is closed. In normal usage, EF Core's `DbContext` opens and closes database connections as needed - every time a query is executed - to avoid keeping connection for unnecessarily long times. However, with in-memory SQLite this would lead to resetting the database every time; so as a workaround, we open the connection before passing it to EF Core, and arrange for it to be closed only when the test completes: + +[!code-csharp[Main](../../../samples/core/Testing/TestingWithoutTheDatabase/SqliteInMemoryBloggingControllerTest.cs?name=ConstructorAndDispose)] + +Tests can now call `CreateContext`, which returns a context using the connection we set up in the constructor, ensuring we have a clean database with the seeded data. + +The full sample code can be viewed [here](https://github.com/dotnet/EntityFramework.Docs/blob/main/samples/core/Testing/TestingWithoutTheDatabase/SqliteInMemoryBloggingControllerTest.cs). + +## In-memory provider + +As discussed in the [testing overview page](xref:core/testing/choosing-a-testing-strategy#inmemory-as-a-database-fake), using the in-memory provider for testing is strongly discouraged; [consider using SQLite instead](#sqlite-in-memory), or [implementing the repository pattern](#repository-pattern). If you've decided to use in-memory, here is a typical test class constructor that sets up and seeds a new in-memory database before each test: + +[!code-csharp[Main](../../../samples/core/Testing/TestingWithoutTheDatabase/InMemoryBloggingControllerTest.cs?name=Constructor)] + +In-memory databases are identified by a simple, string name, and it's possible to connect to the same database several times by providing the same name (this is why the sample above must call `EnsureDeleted` before each test). However, note that in-memory databases are rooted in the context's internal service provider; while in most cases contexts share the same service provider, configuring contexts with different options may trigger the use of a new internal service provider. When that's the case, explicitly pass the same instance of to `UseInMemoryDatabase` for all contexts which should share in-memory databases (this is typically done by having a static `InMemoryDatabaseRoot` field). + +Note that by default, if a transaction is started, the in-memory provider will throw an exception since transactions aren't supported. You may wish to have transactions silently ignored instead, by configuring EF Core to ignore `InMemoryEventId.TransactionIgnoredWarning` as in the above sample. However, if your code actually relies on transactional semantics - e.g. depends on rollback actually rolling back changes - your test won't work. + +The full sample code can be viewed [here](https://github.com/dotnet/EntityFramework.Docs/blob/main/samples/core/Testing/TestingWithoutTheDatabase/InMemoryBloggingControllerTest.cs). diff --git a/entity-framework/toc.yml b/entity-framework/toc.yml index 563ce99074..39ccc8625d 100644 --- a/entity-framework/toc.yml +++ b/entity-framework/toc.yml @@ -288,16 +288,12 @@ - name: Testing items: - - name: Testing code that uses EF Core + - name: Introduction to testing href: core/testing/index.md - - name: EF Core testing sample - href: core/testing/testing-sample.md - - name: Sharing databases between tests - href: core/testing/sharing-databases.md - - name: Test with SQLite - href: core/testing/sqlite.md - - name: Test with InMemory - href: core/testing/in-memory.md + - name: Testing against your production database system + href: core/testing/testing-with-the-database.md + - name: Testing without your production database system + href: core/testing/testing-without-the-database.md - name: Performance items: diff --git a/samples/core/Miscellaneous/Testing/BusinessLogic/BlogService.cs b/samples/core/Miscellaneous/Testing/BusinessLogic/BlogService.cs deleted file mode 100644 index 00ebc6fdf7..0000000000 --- a/samples/core/Miscellaneous/Testing/BusinessLogic/BlogService.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace BusinessLogic -{ - public class BlogService - { - private readonly BloggingContext _context; - - public BlogService(BloggingContext context) - { - _context = context; - } - - public void Add(string url) - { - var blog = new Blog { Url = url }; - _context.Blogs.Add(blog); - _context.SaveChanges(); - } - - public IEnumerable Find(string term) - { - return _context.Blogs - .Where(b => b.Url.Contains(term)) - .OrderBy(b => b.Url) - .ToList(); - } - - public IEnumerable GetAllResources() - { - return _context.Resources - .OrderBy(b => b.Url) - .ToList(); - } - } -} diff --git a/samples/core/Miscellaneous/Testing/BusinessLogic/BloggingContext.cs b/samples/core/Miscellaneous/Testing/BusinessLogic/BloggingContext.cs deleted file mode 100644 index 14e8e67ae4..0000000000 --- a/samples/core/Miscellaneous/Testing/BusinessLogic/BloggingContext.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore; - -namespace BusinessLogic -{ - public class BloggingContext : DbContext - { - private readonly Action _customizeModel; - - #region Constructors - public BloggingContext() - { - } - - public BloggingContext(DbContextOptions options) - : base(options) - { - } - - public BloggingContext(DbContextOptions options, Action customizeModel) - : base(options) - { - // customizeModel must be the same for every instance in a given application. - // Otherwise a custom IModelCacheKeyFactory implementation must be provided. - _customizeModel = customizeModel; - } - #endregion - - public DbSet Blogs { get; set; } - public DbSet Resources { get; set; } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.Entity().HasNoKey() - .ToView("AllResources"); - - if (_customizeModel != null) - { - _customizeModel(this, modelBuilder); - } - } - - #region OnConfiguring - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - if (!optionsBuilder.IsConfigured) - { - optionsBuilder.UseSqlServer( - @"Server=(localdb)\mssqllocaldb;Database=EFProviders.InMemory;Trusted_Connection=True"); - } - } - #endregion - } -} diff --git a/samples/core/Miscellaneous/Testing/BusinessLogic/UrlResource.cs b/samples/core/Miscellaneous/Testing/BusinessLogic/UrlResource.cs deleted file mode 100644 index 2191514d18..0000000000 --- a/samples/core/Miscellaneous/Testing/BusinessLogic/UrlResource.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace BusinessLogic -{ - public class UrlResource - { - public string Url { get; set; } - } -} diff --git a/samples/core/Miscellaneous/Testing/ItemsWebApi/ItemsWebApi/Controllers/ItemsController.cs b/samples/core/Miscellaneous/Testing/ItemsWebApi/ItemsWebApi/Controllers/ItemsController.cs deleted file mode 100644 index d5e439047a..0000000000 --- a/samples/core/Miscellaneous/Testing/ItemsWebApi/ItemsWebApi/Controllers/ItemsController.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; - -namespace Items.Controllers -{ - [ApiController] - [Route("[controller]")] - public class ItemsController : ControllerBase - { - #region Constructor - private readonly ItemsContext _context; - - public ItemsController(ItemsContext context) - => _context = context; - #endregion - - #region Get - [HttpGet] - public IEnumerable Get() - => _context.Set().Include(e => e.Tags).OrderBy(e => e.Name); - - [HttpGet] - public Item Get(string itemName) - => _context.Set().Include(e => e.Tags).FirstOrDefault(e => e.Name == itemName); - #endregion - - #region PostItem - [HttpPost] - public ActionResult PostItem(string itemName) - { - var item = _context.Add(new Item(itemName)).Entity; - - _context.SaveChanges(); - - return item; - } - #endregion - - #region PostTag - [HttpPost] - public ActionResult PostTag(string itemName, string tagLabel) - { - var tag = _context - .Set() - .Include(e => e.Tags) - .Single(e => e.Name == itemName) - .AddTag(tagLabel); - - _context.SaveChanges(); - - return tag; - } - #endregion - - #region DeleteItem - [HttpDelete("{itemName}")] - public ActionResult DeleteItem(string itemName) - { - var item = _context - .Set() - .SingleOrDefault(e => e.Name == itemName); - - if (item == null) - { - return NotFound(); - } - - _context.Remove(item); - _context.SaveChanges(); - - return item; - } - #endregion - } -} diff --git a/samples/core/Miscellaneous/Testing/ItemsWebApi/ItemsWebApi/Item.cs b/samples/core/Miscellaneous/Testing/ItemsWebApi/ItemsWebApi/Item.cs deleted file mode 100644 index 6d3efed647..0000000000 --- a/samples/core/Miscellaneous/Testing/ItemsWebApi/ItemsWebApi/Item.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace Items -{ - #region ItemEntityType - public class Item - { - private readonly int _id; - private readonly List _tags = new List(); - - private Item(int id, string name) - { - _id = id; - Name = name; - } - - public Item(string name) - { - Name = name; - } - - public Tag AddTag(string label) - { - var tag = _tags.FirstOrDefault(t => t.Label == label); - - if (tag == null) - { - tag = new Tag(label); - _tags.Add(tag); - } - - tag.Count++; - - return tag; - } - - public string Name { get; } - - public IReadOnlyList Tags => _tags; - } - #endregion -} diff --git a/samples/core/Miscellaneous/Testing/ItemsWebApi/ItemsWebApi/ItemsContext.cs b/samples/core/Miscellaneous/Testing/ItemsWebApi/ItemsWebApi/ItemsContext.cs deleted file mode 100644 index fc5d1afca2..0000000000 --- a/samples/core/Miscellaneous/Testing/ItemsWebApi/ItemsWebApi/ItemsContext.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace Items -{ - public class ItemsContext : DbContext - { - public ItemsContext(DbContextOptions options) - : base(options) - { - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - #region ConfigureItem - modelBuilder.Entity( - b => - { - b.Property("_id"); - b.HasKey("_id"); - b.Property(e => e.Name); - b.HasMany(e => e.Tags).WithOne().IsRequired(); - }); - #endregion - - #region ConfigureTag - modelBuilder.Entity( - b => - { - b.Property("_id"); - b.HasKey("_id"); - b.Property(e => e.Label); - }); - #endregion - } - } -} diff --git a/samples/core/Miscellaneous/Testing/ItemsWebApi/ItemsWebApi/ItemsWebApi.csproj b/samples/core/Miscellaneous/Testing/ItemsWebApi/ItemsWebApi/ItemsWebApi.csproj deleted file mode 100644 index 6b0d1c254a..0000000000 --- a/samples/core/Miscellaneous/Testing/ItemsWebApi/ItemsWebApi/ItemsWebApi.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - net6.0 - Items - - - - - - - diff --git a/samples/core/Miscellaneous/Testing/ItemsWebApi/ItemsWebApi/Tag.cs b/samples/core/Miscellaneous/Testing/ItemsWebApi/ItemsWebApi/Tag.cs deleted file mode 100644 index 02084f9c68..0000000000 --- a/samples/core/Miscellaneous/Testing/ItemsWebApi/ItemsWebApi/Tag.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace Items -{ - #region TagEntityType - public class Tag - { - private readonly int _id; - - private Tag(int id, string label) - { - _id = id; - Label = label; - } - - public Tag(string label) => Label = label; - - public string Label { get; } - - public int Count { get; set; } - } - #endregion -} diff --git a/samples/core/Miscellaneous/Testing/ItemsWebApi/SharedDatabaseTests/SharedDatabaseFixture.cs b/samples/core/Miscellaneous/Testing/ItemsWebApi/SharedDatabaseTests/SharedDatabaseFixture.cs deleted file mode 100644 index c7072afdcb..0000000000 --- a/samples/core/Miscellaneous/Testing/ItemsWebApi/SharedDatabaseTests/SharedDatabaseFixture.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using System.Data.Common; -using Items; -using Microsoft.Data.SqlClient; -using Microsoft.EntityFrameworkCore; - -namespace SharedDatabaseTests -{ - #region SharedDatabaseFixture - public class SharedDatabaseFixture : IDisposable - { - private static readonly object _lock = new object(); - private static bool _databaseInitialized; - - public SharedDatabaseFixture() - { - Connection = new SqlConnection(@"Server=(localdb)\mssqllocaldb;Database=EFTestSample;Trusted_Connection=True"); - - Seed(); - - Connection.Open(); - } - - public DbConnection Connection { get; } - - public ItemsContext CreateContext(DbTransaction transaction = null) - { - var context = new ItemsContext(new DbContextOptionsBuilder().UseSqlServer(Connection).Options); - - if (transaction != null) - { - context.Database.UseTransaction(transaction); - } - - return context; - } - - private void Seed() - { - lock (_lock) - { - if (!_databaseInitialized) - { - using (var context = CreateContext()) - { - context.Database.EnsureDeleted(); - context.Database.EnsureCreated(); - - var one = new Item("ItemOne"); - one.AddTag("Tag11"); - one.AddTag("Tag12"); - one.AddTag("Tag13"); - - var two = new Item("ItemTwo"); - - var three = new Item("ItemThree"); - three.AddTag("Tag31"); - three.AddTag("Tag31"); - three.AddTag("Tag31"); - three.AddTag("Tag32"); - three.AddTag("Tag32"); - - context.AddRange(one, two, three); - - context.SaveChanges(); - } - - _databaseInitialized = true; - } - } - } - - public void Dispose() => Connection.Dispose(); - } - #endregion -} diff --git a/samples/core/Miscellaneous/Testing/ItemsWebApi/SharedDatabaseTests/SharedDatabaseTest.cs b/samples/core/Miscellaneous/Testing/ItemsWebApi/SharedDatabaseTests/SharedDatabaseTest.cs deleted file mode 100644 index 355a3ce535..0000000000 --- a/samples/core/Miscellaneous/Testing/ItemsWebApi/SharedDatabaseTests/SharedDatabaseTest.cs +++ /dev/null @@ -1,156 +0,0 @@ -using System.Linq; -using Items; -using Items.Controllers; -using Microsoft.EntityFrameworkCore; -using Xunit; - -namespace SharedDatabaseTests -{ - #region UsingTheFixture - public class SharedDatabaseTest : IClassFixture - { - public SharedDatabaseTest(SharedDatabaseFixture fixture) => Fixture = fixture; - - public SharedDatabaseFixture Fixture { get; } - #endregion - - #region CanGetItems - [Fact] - public void Can_get_items() - { - using (var context = Fixture.CreateContext()) - { - var controller = new ItemsController(context); - - var items = controller.Get().ToList(); - - Assert.Equal(3, items.Count); - Assert.Equal("ItemOne", items[0].Name); - Assert.Equal("ItemThree", items[1].Name); - Assert.Equal("ItemTwo", items[2].Name); - } - } - #endregion - - [Fact] - public void Can_get_item() - { - using (var context = Fixture.CreateContext()) - { - var controller = new ItemsController(context); - - var item = controller.Get("ItemTwo"); - - Assert.Equal("ItemTwo", item.Name); - } - } - - #region CanAddItem - [Fact] - public void Can_add_item() - { - using (var transaction = Fixture.Connection.BeginTransaction()) - { - using (var context = Fixture.CreateContext(transaction)) - { - var controller = new ItemsController(context); - - var item = controller.PostItem("ItemFour").Value; - - Assert.Equal("ItemFour", item.Name); - } - - using (var context = Fixture.CreateContext(transaction)) - { - var item = context.Set().Single(e => e.Name == "ItemFour"); - - Assert.Equal("ItemFour", item.Name); - Assert.Equal(0, item.Tags.Count); - } - } - } - #endregion - - #region CanAddTag - [Fact] - public void Can_add_tag() - { - using (var transaction = Fixture.Connection.BeginTransaction()) - { - using (var context = Fixture.CreateContext(transaction)) - { - var controller = new ItemsController(context); - - var tag = controller.PostTag("ItemTwo", "Tag21").Value; - - Assert.Equal("Tag21", tag.Label); - Assert.Equal(1, tag.Count); - } - - using (var context = Fixture.CreateContext(transaction)) - { - var item = context.Set().Include(e => e.Tags).Single(e => e.Name == "ItemTwo"); - - Assert.Equal(1, item.Tags.Count); - Assert.Equal("Tag21", item.Tags[0].Label); - Assert.Equal(1, item.Tags[0].Count); - } - } - } - #endregion - - #region CanUpTagCount - [Fact] - public void Can_add_tag_when_already_existing_tag() - { - using (var transaction = Fixture.Connection.BeginTransaction()) - { - using (var context = Fixture.CreateContext(transaction)) - { - var controller = new ItemsController(context); - - var tag = controller.PostTag("ItemThree", "Tag32").Value; - - Assert.Equal("Tag32", tag.Label); - Assert.Equal(3, tag.Count); - } - - using (var context = Fixture.CreateContext(transaction)) - { - var item = context.Set().Include(e => e.Tags).Single(e => e.Name == "ItemThree"); - - Assert.Equal(2, item.Tags.Count); - Assert.Equal("Tag31", item.Tags[0].Label); - Assert.Equal(3, item.Tags[0].Count); - Assert.Equal("Tag32", item.Tags[1].Label); - Assert.Equal(3, item.Tags[1].Count); - } - } - } - #endregion - - #region DeleteItem - [Fact] - public void Can_remove_item_and_all_associated_tags() - { - using (var transaction = Fixture.Connection.BeginTransaction()) - { - using (var context = Fixture.CreateContext(transaction)) - { - var controller = new ItemsController(context); - - var item = controller.DeleteItem("ItemThree").Value; - - Assert.Equal("ItemThree", item.Name); - } - - using (var context = Fixture.CreateContext(transaction)) - { - Assert.False(context.Set().Any(e => e.Name == "ItemThree")); - Assert.False(context.Set().Any(e => e.Label.StartsWith("Tag3"))); - } - } - } - #endregion - } -} diff --git a/samples/core/Miscellaneous/Testing/ItemsWebApi/SharedDatabaseTests/SharedDatabaseTests.csproj b/samples/core/Miscellaneous/Testing/ItemsWebApi/SharedDatabaseTests/SharedDatabaseTests.csproj deleted file mode 100644 index 0b19de27fa..0000000000 --- a/samples/core/Miscellaneous/Testing/ItemsWebApi/SharedDatabaseTests/SharedDatabaseTests.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - net6.0 - - - - - - - - - - - - - - diff --git a/samples/core/Miscellaneous/Testing/ItemsWebApi/Tests/InMemoryItemsControllerTest.cs b/samples/core/Miscellaneous/Testing/ItemsWebApi/Tests/InMemoryItemsControllerTest.cs deleted file mode 100644 index 6ac4840eb4..0000000000 --- a/samples/core/Miscellaneous/Testing/ItemsWebApi/Tests/InMemoryItemsControllerTest.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Items; -using Microsoft.EntityFrameworkCore; - -namespace Tests -{ - public class InMemoryItemsControllerTest : ItemsControllerTest - { - public InMemoryItemsControllerTest() - : base( - new DbContextOptionsBuilder() - .UseInMemoryDatabase("TestDatabase") - .Options) - { - } - } -} diff --git a/samples/core/Miscellaneous/Testing/ItemsWebApi/Tests/ItemsControllerTest.cs b/samples/core/Miscellaneous/Testing/ItemsWebApi/Tests/ItemsControllerTest.cs deleted file mode 100644 index 9a33a612f8..0000000000 --- a/samples/core/Miscellaneous/Testing/ItemsWebApi/Tests/ItemsControllerTest.cs +++ /dev/null @@ -1,198 +0,0 @@ -using System.Linq; -using Items; -using Items.Controllers; -using Microsoft.EntityFrameworkCore; -using Xunit; - -namespace Tests -{ - public abstract class ItemsControllerTest - { - #region Seeding - protected ItemsControllerTest(DbContextOptions contextOptions) - { - ContextOptions = contextOptions; - - Seed(); - } - - protected DbContextOptions ContextOptions { get; } - - private void Seed() - { - using (var context = new ItemsContext(ContextOptions)) - { - context.Database.EnsureDeleted(); - context.Database.EnsureCreated(); - - var one = new Item("ItemOne"); - one.AddTag("Tag11"); - one.AddTag("Tag12"); - one.AddTag("Tag13"); - - var two = new Item("ItemTwo"); - - var three = new Item("ItemThree"); - three.AddTag("Tag31"); - three.AddTag("Tag31"); - three.AddTag("Tag31"); - three.AddTag("Tag32"); - three.AddTag("Tag32"); - - context.AddRange(one, two, three); - - context.SaveChanges(); - } - } - #endregion - - #region CanGetItems - [Fact] - public void Can_get_items() - { - using (var context = new ItemsContext(ContextOptions)) - { - var controller = new ItemsController(context); - - var items = controller.Get().ToList(); - - Assert.Equal(3, items.Count); - Assert.Equal("ItemOne", items[0].Name); - Assert.Equal("ItemThree", items[1].Name); - Assert.Equal("ItemTwo", items[2].Name); - } - } - #endregion - - [Fact] - public void Can_get_item() - { - using (var context = new ItemsContext(ContextOptions)) - { - var controller = new ItemsController(context); - - var item = controller.Get("ItemTwo"); - - Assert.Equal("ItemTwo", item.Name); - } - } - - #region CanAddItem - [Fact] - public void Can_add_item() - { - using (var context = new ItemsContext(ContextOptions)) - { - var controller = new ItemsController(context); - - var item = controller.PostItem("ItemFour").Value; - - Assert.Equal("ItemFour", item.Name); - } - - using (var context = new ItemsContext(ContextOptions)) - { - var item = context.Set().Single(e => e.Name == "ItemFour"); - - Assert.Equal("ItemFour", item.Name); - Assert.Equal(0, item.Tags.Count); - } - } - #endregion - - #region CanAddItemCaseInsensitive - [Fact] - public void Can_add_item_differing_only_by_case() - { - using (var context = new ItemsContext(ContextOptions)) - { - var controller = new ItemsController(context); - - var item = controller.PostItem("itemtwo").Value; - - Assert.Equal("itemtwo", item.Name); - } - - using (var context = new ItemsContext(ContextOptions)) - { - var item = context.Set().Single(e => e.Name == "itemtwo"); - - Assert.Equal(0, item.Tags.Count); - } - } - #endregion - - #region CanAddTag - [Fact] - public void Can_add_tag() - { - using (var context = new ItemsContext(ContextOptions)) - { - var controller = new ItemsController(context); - - var tag = controller.PostTag("ItemTwo", "Tag21").Value; - - Assert.Equal("Tag21", tag.Label); - Assert.Equal(1, tag.Count); - } - - using (var context = new ItemsContext(ContextOptions)) - { - var item = context.Set().Include(e => e.Tags).Single(e => e.Name == "ItemTwo"); - - Assert.Equal(1, item.Tags.Count); - Assert.Equal("Tag21", item.Tags[0].Label); - Assert.Equal(1, item.Tags[0].Count); - } - } - #endregion - - #region CanUpTagCount - [Fact] - public void Can_add_tag_when_already_existing_tag() - { - using (var context = new ItemsContext(ContextOptions)) - { - var controller = new ItemsController(context); - - var tag = controller.PostTag("ItemThree", "Tag32").Value; - - Assert.Equal("Tag32", tag.Label); - Assert.Equal(3, tag.Count); - } - - using (var context = new ItemsContext(ContextOptions)) - { - var item = context.Set().Include(e => e.Tags).Single(e => e.Name == "ItemThree"); - - Assert.Equal(2, item.Tags.Count); - Assert.Equal("Tag31", item.Tags[0].Label); - Assert.Equal(3, item.Tags[0].Count); - Assert.Equal("Tag32", item.Tags[1].Label); - Assert.Equal(3, item.Tags[1].Count); - } - } - #endregion - - #region DeleteItem - [Fact] - public void Can_remove_item_and_all_associated_tags() - { - using (var context = new ItemsContext(ContextOptions)) - { - var controller = new ItemsController(context); - - var item = controller.DeleteItem("ItemThree").Value; - - Assert.Equal("ItemThree", item.Name); - } - - using (var context = new ItemsContext(ContextOptions)) - { - Assert.False(context.Set().Any(e => e.Name == "ItemThree")); - Assert.False(context.Set().Any(e => e.Label.StartsWith("Tag3"))); - } - } - #endregion - } -} diff --git a/samples/core/Miscellaneous/Testing/ItemsWebApi/Tests/SqlServerItemsControllerTest.cs b/samples/core/Miscellaneous/Testing/ItemsWebApi/Tests/SqlServerItemsControllerTest.cs deleted file mode 100644 index 466f1b5da0..0000000000 --- a/samples/core/Miscellaneous/Testing/ItemsWebApi/Tests/SqlServerItemsControllerTest.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Items; -using Microsoft.EntityFrameworkCore; - -namespace Tests -{ - public class SqlServerItemsControllerTest : ItemsControllerTest - { - public SqlServerItemsControllerTest() - : base( - new DbContextOptionsBuilder() - .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFTestSample;Trusted_Connection=True") - .Options) - { - } - } -} diff --git a/samples/core/Miscellaneous/Testing/ItemsWebApi/Tests/SqliteInMemoryItemsControllerTest.cs b/samples/core/Miscellaneous/Testing/ItemsWebApi/Tests/SqliteInMemoryItemsControllerTest.cs deleted file mode 100644 index e4ed2ea306..0000000000 --- a/samples/core/Miscellaneous/Testing/ItemsWebApi/Tests/SqliteInMemoryItemsControllerTest.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Data.Common; -using Items; -using Microsoft.Data.Sqlite; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; - -namespace Tests -{ - #region SqliteInMemory - public class SqliteInMemoryItemsControllerTest : ItemsControllerTest, IDisposable - { - private readonly DbConnection _connection; - - public SqliteInMemoryItemsControllerTest() - : base( - new DbContextOptionsBuilder() - .UseSqlite(CreateInMemoryDatabase()) - .Options) - { - _connection = RelationalOptionsExtension.Extract(ContextOptions).Connection; - } - - private static DbConnection CreateInMemoryDatabase() - { - var connection = new SqliteConnection("Filename=:memory:"); - - connection.Open(); - - return connection; - } - - public void Dispose() => _connection.Dispose(); - } - #endregion -} diff --git a/samples/core/Miscellaneous/Testing/ItemsWebApi/Tests/SqliteItemsControllerTest.cs b/samples/core/Miscellaneous/Testing/ItemsWebApi/Tests/SqliteItemsControllerTest.cs deleted file mode 100644 index 1376e4c2ef..0000000000 --- a/samples/core/Miscellaneous/Testing/ItemsWebApi/Tests/SqliteItemsControllerTest.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Items; -using Microsoft.EntityFrameworkCore; - -namespace Tests -{ - #region SqliteItemsControllerTest - public class SqliteItemsControllerTest : ItemsControllerTest - { - public SqliteItemsControllerTest() - : base( - new DbContextOptionsBuilder() - .UseSqlite("Filename=Test.db") - .Options) - { - } - } - #endregion -} diff --git a/samples/core/Miscellaneous/Testing/ItemsWebApi/Tests/Tests.csproj b/samples/core/Miscellaneous/Testing/ItemsWebApi/Tests/Tests.csproj deleted file mode 100644 index b666bca43f..0000000000 --- a/samples/core/Miscellaneous/Testing/ItemsWebApi/Tests/Tests.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - net6.0 - - - - - - - - - - - - - - - - diff --git a/samples/core/Miscellaneous/Testing/TestProject/InMemory/BlogServiceTests.cs b/samples/core/Miscellaneous/Testing/TestProject/InMemory/BlogServiceTests.cs deleted file mode 100644 index 75787907db..0000000000 --- a/samples/core/Miscellaneous/Testing/TestProject/InMemory/BlogServiceTests.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System.Linq; -using BusinessLogic; -using Microsoft.EntityFrameworkCore; -using Xunit; - -namespace EFTesting.TestProject.InMemory -{ - public class BlogServiceTests - { - [Fact] - public void Add_writes_to_database() - { - var options = new DbContextOptionsBuilder() - .UseInMemoryDatabase(databaseName: "Add_writes_to_database") - .Options; - - // Run the test against one instance of the context - using (var context = CreateContext(options)) - { - var service = new BlogService(context); - service.Add("https://example.com"); - context.SaveChanges(); - } - - // Use a separate instance of the context to verify correct data was saved to database - using (var context = CreateContext(options)) - { - Assert.Equal(1, context.Blogs.Count()); - Assert.Equal("https://example.com", context.Blogs.Single().Url); - } - } - - [Fact] - public void Find_searches_url() - { - var options = new DbContextOptionsBuilder() - .UseInMemoryDatabase(databaseName: "Find_searches_url") - .Options; - - // Insert seed data into the database using one instance of the context - using (var context = CreateContext(options)) - { - context.Blogs.Add(new Blog { Url = "https://example.com/cats" }); - context.Blogs.Add(new Blog { Url = "https://example.com/catfish" }); - context.Blogs.Add(new Blog { Url = "https://example.com/dogs" }); - context.SaveChanges(); - } - - // Use a clean instance of the context to run the test - using (var context = CreateContext(options)) - { - var service = new BlogService(context); - var result = service.Find("cat"); - Assert.Equal(2, result.Count()); - } - } - - [Fact] - public void GetAllResources_returns_all_resources() - { - var options = new DbContextOptionsBuilder() - .UseInMemoryDatabase(databaseName: "GetAllResources_returns_all_resources") - .Options; - - // Insert seed data into the database using one instance of the context - using (var context = CreateContext(options)) - { - context.Blogs.Add(new Blog { Url = "https://example.com/cats" }); - context.Blogs.Add(new Blog { Url = "https://example.com/catfish" }); - context.Blogs.Add(new Blog { Url = "https://example.com/dogs" }); - context.SaveChanges(); - } - - // Use a clean instance of the context to run the test - using (var context = CreateContext(options)) - { - var service = new BlogService(context); - var result = service.GetAllResources(); - Assert.Equal(3, result.Count()); - } - } - - private static BloggingContext CreateContext(DbContextOptions options) - => new BloggingContext( - options, (context, modelBuilder) => - { - modelBuilder.Entity() - .ToInMemoryQuery(() => context.Blogs.Select(b => new UrlResource { Url = b.Url })); - }); - } -} diff --git a/samples/core/Miscellaneous/Testing/TestProject/SQLite/BlogServiceTests.cs b/samples/core/Miscellaneous/Testing/TestProject/SQLite/BlogServiceTests.cs deleted file mode 100644 index f7f2ea31b2..0000000000 --- a/samples/core/Miscellaneous/Testing/TestProject/SQLite/BlogServiceTests.cs +++ /dev/null @@ -1,148 +0,0 @@ -using System.Linq; -using BusinessLogic; -using Microsoft.Data.Sqlite; -using Microsoft.EntityFrameworkCore; -using Xunit; - -namespace EFTesting.TestProject.SQLite -{ - public class BlogServiceTests - { - [Fact] - public void Add_writes_to_database() - { - // In-memory database only exists while the connection is open - var connection = new SqliteConnection("DataSource=:memory:"); - connection.Open(); - - try - { - var options = new DbContextOptionsBuilder() - .UseSqlite(connection) - .Options; - - // Create the schema in the database - using (var context = new BloggingContext(options)) - { - EnsureCreated(context); - } - - // Run the test against one instance of the context - using (var context = new BloggingContext(options)) - { - var service = new BlogService(context); - service.Add("https://example.com"); - context.SaveChanges(); - } - - // Use a separate instance of the context to verify correct data was saved to database - using (var context = new BloggingContext(options)) - { - Assert.Equal(1, context.Blogs.Count()); - Assert.Equal("https://example.com", context.Blogs.Single().Url); - } - } - finally - { - connection.Close(); - } - } - - [Fact] - public void Find_searches_url() - { - // In-memory database only exists while the connection is open - var connection = new SqliteConnection("DataSource=:memory:"); - connection.Open(); - - try - { - var options = new DbContextOptionsBuilder() - .UseSqlite(connection) - .Options; - - // Create the schema in the database - using (var context = new BloggingContext(options)) - { - EnsureCreated(context); - } - - // Insert seed data into the database using one instance of the context - using (var context = new BloggingContext(options)) - { - context.Blogs.Add(new Blog { Url = "https://example.com/cats" }); - context.Blogs.Add(new Blog { Url = "https://example.com/catfish" }); - context.Blogs.Add(new Blog { Url = "https://example.com/dogs" }); - context.SaveChanges(); - } - - // Use a clean instance of the context to run the test - using (var context = new BloggingContext(options)) - { - var service = new BlogService(context); - var result = service.Find("cat"); - Assert.Equal(2, result.Count()); - } - } - finally - { - connection.Close(); - } - } - - [Fact] - public void GetAllResources_returns_all_resources() - { - // In-memory database only exists while the connection is open - var connection = new SqliteConnection("DataSource=:memory:"); - connection.Open(); - - try - { - var options = new DbContextOptionsBuilder() - .UseSqlite(connection) - .Options; - - // Create the schema in the database - using (var context = new BloggingContext(options)) - { - EnsureCreated(context); - } - - // Insert seed data into the database using one instance of the context - using (var context = new BloggingContext(options)) - { - context.Blogs.Add(new Blog { Url = "https://example.com/cats" }); - context.Blogs.Add(new Blog { Url = "https://example.com/catfish" }); - context.Blogs.Add(new Blog { Url = "https://example.com/dogs" }); - context.SaveChanges(); - } - - // Use a clean instance of the context to run the test - using (var context = new BloggingContext(options)) - { - var service = new BlogService(context); - var result = service.GetAllResources(); - Assert.Equal(3, result.Count()); - } - } - finally - { - connection.Close(); - } - } - - private static void EnsureCreated(BloggingContext context) - { - if (context.Database.EnsureCreated()) - { - using var viewCommand = context.Database.GetDbConnection().CreateCommand(); - viewCommand.CommandText = @" -CREATE VIEW AllResources AS -SELECT Url -FROM Blogs;"; - viewCommand.ExecuteNonQuery(); - } - } - } -} diff --git a/samples/core/Miscellaneous/Testing/TestProject/TestProject.csproj b/samples/core/Miscellaneous/Testing/TestProject/TestProject.csproj deleted file mode 100644 index a5bf0f81fe..0000000000 --- a/samples/core/Miscellaneous/Testing/TestProject/TestProject.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - net6.0 - EFTesting.TestProject - EFTesting.TestProject - - - - - - - - - - - - - - - diff --git a/samples/core/Samples.sln b/samples/core/Samples.sln index 11a63f21eb..ee746a87f6 100644 --- a/samples/core/Samples.sln +++ b/samples/core/Samples.sln @@ -3,18 +3,12 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31815.197 MinimumVisualStudioVersion = 17.0.31815.197 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Testing", "Testing", "{4E2B02EE-0C76-42D6-BA0A-337D7680A5D6}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Miscellaneous", "Miscellaneous", "{85AFD7F1-6943-40FE-B8EC-AA9DBB42CCA6}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Modeling", "Modeling", "{CA5046EC-C894-4535-8190-A31F75FDEB96}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Saving", "Saving\Saving.csproj", "{353F06F1-F0E0-4A8F-83AA-7115CD05832D}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestProject", "Miscellaneous\Testing\TestProject\TestProject.csproj", "{F4468750-9480-48E0-AA7A-DE84780AE1C2}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BusinessLogic", "Miscellaneous\Testing\BusinessLogic\BusinessLogic.csproj", "{60B2C7AF-655F-4709-9E7F-7403E61F3A08}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Logging", "Miscellaneous\Logging\Logging\Logging.csproj", "{D3A5391D-9351-44D6-8122-2821C6E4BD62}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConnectionResiliency", "Miscellaneous\ConnectionResiliency\ConnectionResiliency.csproj", "{73D58479-A7E6-4867-8A73-0E07E96C6117}" @@ -37,14 +31,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SqlServer", "SqlServer\SqlS EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ValueConversions", "Modeling\ValueConversions\ValueConversions.csproj", "{FE71504E-C32B-4E2F-9830-21ED448DABC4}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ItemsWebApi", "Miscellaneous\Testing\ItemsWebApi\ItemsWebApi\ItemsWebApi.csproj", "{ECF03060-646F-4B62-9446-1953F228CB09}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ItemsWebApi", "ItemsWebApi", "{8695C7BE-F9B2-477A-AD7B-C15DC5418F66}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "Miscellaneous\Testing\ItemsWebApi\Tests\Tests.csproj", "{E8AD02D7-8AFB-4233-BDAF-F0AEF986F1F3}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharedDatabaseTests", "Miscellaneous\Testing\ItemsWebApi\SharedDatabaseTests\SharedDatabaseTests.csproj", "{34C237C8-DD12-4C14-9B15-B7F85C218CDB}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Collations", "Miscellaneous\Collations\Collations.csproj", "{62C86664-49F4-4C59-A2EC-1D70D85149D9}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Async", "Miscellaneous\Async\Async.csproj", "{1DA2B6AD-F71A-4224-92EB-3D0EE6E68BF4}" @@ -179,6 +165,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BulkConfiguration", "Modeli EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pagination", "Querying\Pagination\Pagination.csproj", "{A7A02F2B-36E1-46A5-AF1F-E58E99E73324}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Testing", "Testing", "{06678E46-B4E2-432B-A46E-56DEEF43F6CE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BusinessLogic", "Testing\BusinessLogic\BusinessLogic.csproj", "{487B9A76-363D-441F-84B6-ADEDEA1F2E39}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestingWithTheDatabase", "Testing\TestingWithTheDatabase\TestingWithTheDatabase.csproj", "{E056C05D-9BAD-4B69-9751-5C26D76319A4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BloggingWebApi", "Testing\BloggingWebApi\BloggingWebApi.csproj", "{046C7454-643D-4C83-A2C7-50E060446B18}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestingWithoutTheDatabase", "Testing\TestingWithoutTheDatabase\TestingWithoutTheDatabase.csproj", "{937DD70A-FF53-4129-A7C5-FD5F0758591D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -189,14 +185,6 @@ Global {353F06F1-F0E0-4A8F-83AA-7115CD05832D}.Debug|Any CPU.Build.0 = Debug|Any CPU {353F06F1-F0E0-4A8F-83AA-7115CD05832D}.Release|Any CPU.ActiveCfg = Release|Any CPU {353F06F1-F0E0-4A8F-83AA-7115CD05832D}.Release|Any CPU.Build.0 = Release|Any CPU - {F4468750-9480-48E0-AA7A-DE84780AE1C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F4468750-9480-48E0-AA7A-DE84780AE1C2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F4468750-9480-48E0-AA7A-DE84780AE1C2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F4468750-9480-48E0-AA7A-DE84780AE1C2}.Release|Any CPU.Build.0 = Release|Any CPU - {60B2C7AF-655F-4709-9E7F-7403E61F3A08}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {60B2C7AF-655F-4709-9E7F-7403E61F3A08}.Debug|Any CPU.Build.0 = Debug|Any CPU - {60B2C7AF-655F-4709-9E7F-7403E61F3A08}.Release|Any CPU.ActiveCfg = Release|Any CPU - {60B2C7AF-655F-4709-9E7F-7403E61F3A08}.Release|Any CPU.Build.0 = Release|Any CPU {D3A5391D-9351-44D6-8122-2821C6E4BD62}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D3A5391D-9351-44D6-8122-2821C6E4BD62}.Debug|Any CPU.Build.0 = Debug|Any CPU {D3A5391D-9351-44D6-8122-2821C6E4BD62}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -241,18 +229,6 @@ Global {FE71504E-C32B-4E2F-9830-21ED448DABC4}.Debug|Any CPU.Build.0 = Debug|Any CPU {FE71504E-C32B-4E2F-9830-21ED448DABC4}.Release|Any CPU.ActiveCfg = Release|Any CPU {FE71504E-C32B-4E2F-9830-21ED448DABC4}.Release|Any CPU.Build.0 = Release|Any CPU - {ECF03060-646F-4B62-9446-1953F228CB09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {ECF03060-646F-4B62-9446-1953F228CB09}.Debug|Any CPU.Build.0 = Debug|Any CPU - {ECF03060-646F-4B62-9446-1953F228CB09}.Release|Any CPU.ActiveCfg = Release|Any CPU - {ECF03060-646F-4B62-9446-1953F228CB09}.Release|Any CPU.Build.0 = Release|Any CPU - {E8AD02D7-8AFB-4233-BDAF-F0AEF986F1F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E8AD02D7-8AFB-4233-BDAF-F0AEF986F1F3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E8AD02D7-8AFB-4233-BDAF-F0AEF986F1F3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E8AD02D7-8AFB-4233-BDAF-F0AEF986F1F3}.Release|Any CPU.Build.0 = Release|Any CPU - {34C237C8-DD12-4C14-9B15-B7F85C218CDB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {34C237C8-DD12-4C14-9B15-B7F85C218CDB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {34C237C8-DD12-4C14-9B15-B7F85C218CDB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {34C237C8-DD12-4C14-9B15-B7F85C218CDB}.Release|Any CPU.Build.0 = Release|Any CPU {62C86664-49F4-4C59-A2EC-1D70D85149D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {62C86664-49F4-4C59-A2EC-1D70D85149D9}.Debug|Any CPU.Build.0 = Debug|Any CPU {62C86664-49F4-4C59-A2EC-1D70D85149D9}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -497,14 +473,27 @@ Global {A7A02F2B-36E1-46A5-AF1F-E58E99E73324}.Debug|Any CPU.Build.0 = Debug|Any CPU {A7A02F2B-36E1-46A5-AF1F-E58E99E73324}.Release|Any CPU.ActiveCfg = Release|Any CPU {A7A02F2B-36E1-46A5-AF1F-E58E99E73324}.Release|Any CPU.Build.0 = Release|Any CPU + {487B9A76-363D-441F-84B6-ADEDEA1F2E39}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {487B9A76-363D-441F-84B6-ADEDEA1F2E39}.Debug|Any CPU.Build.0 = Debug|Any CPU + {487B9A76-363D-441F-84B6-ADEDEA1F2E39}.Release|Any CPU.ActiveCfg = Release|Any CPU + {487B9A76-363D-441F-84B6-ADEDEA1F2E39}.Release|Any CPU.Build.0 = Release|Any CPU + {E056C05D-9BAD-4B69-9751-5C26D76319A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E056C05D-9BAD-4B69-9751-5C26D76319A4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E056C05D-9BAD-4B69-9751-5C26D76319A4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E056C05D-9BAD-4B69-9751-5C26D76319A4}.Release|Any CPU.Build.0 = Release|Any CPU + {046C7454-643D-4C83-A2C7-50E060446B18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {046C7454-643D-4C83-A2C7-50E060446B18}.Debug|Any CPU.Build.0 = Debug|Any CPU + {046C7454-643D-4C83-A2C7-50E060446B18}.Release|Any CPU.ActiveCfg = Release|Any CPU + {046C7454-643D-4C83-A2C7-50E060446B18}.Release|Any CPU.Build.0 = Release|Any CPU + {937DD70A-FF53-4129-A7C5-FD5F0758591D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {937DD70A-FF53-4129-A7C5-FD5F0758591D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {937DD70A-FF53-4129-A7C5-FD5F0758591D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {937DD70A-FF53-4129-A7C5-FD5F0758591D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {4E2B02EE-0C76-42D6-BA0A-337D7680A5D6} = {85AFD7F1-6943-40FE-B8EC-AA9DBB42CCA6} - {F4468750-9480-48E0-AA7A-DE84780AE1C2} = {4E2B02EE-0C76-42D6-BA0A-337D7680A5D6} - {60B2C7AF-655F-4709-9E7F-7403E61F3A08} = {4E2B02EE-0C76-42D6-BA0A-337D7680A5D6} {D3A5391D-9351-44D6-8122-2821C6E4BD62} = {85AFD7F1-6943-40FE-B8EC-AA9DBB42CCA6} {73D58479-A7E6-4867-8A73-0E07E96C6117} = {85AFD7F1-6943-40FE-B8EC-AA9DBB42CCA6} {70D00673-084C-40B7-B0F1-67498593F8EE} = {CA5046EC-C894-4535-8190-A31F75FDEB96} @@ -512,10 +501,6 @@ Global {802E31AD-2F1E-41A1-A662-5929E2626601} = {CA5046EC-C894-4535-8190-A31F75FDEB96} {63685B9A-1233-4B44-AAC1-8DDD4B16B65D} = {CA5046EC-C894-4535-8190-A31F75FDEB96} {FE71504E-C32B-4E2F-9830-21ED448DABC4} = {CA5046EC-C894-4535-8190-A31F75FDEB96} - {ECF03060-646F-4B62-9446-1953F228CB09} = {8695C7BE-F9B2-477A-AD7B-C15DC5418F66} - {8695C7BE-F9B2-477A-AD7B-C15DC5418F66} = {4E2B02EE-0C76-42D6-BA0A-337D7680A5D6} - {E8AD02D7-8AFB-4233-BDAF-F0AEF986F1F3} = {8695C7BE-F9B2-477A-AD7B-C15DC5418F66} - {34C237C8-DD12-4C14-9B15-B7F85C218CDB} = {8695C7BE-F9B2-477A-AD7B-C15DC5418F66} {62C86664-49F4-4C59-A2EC-1D70D85149D9} = {85AFD7F1-6943-40FE-B8EC-AA9DBB42CCA6} {1DA2B6AD-F71A-4224-92EB-3D0EE6E68BF4} = {85AFD7F1-6943-40FE-B8EC-AA9DBB42CCA6} {70E581C3-38BB-46CC-9063-ADF9F2B76570} = {85AFD7F1-6943-40FE-B8EC-AA9DBB42CCA6} @@ -575,6 +560,10 @@ Global {8A45191D-F719-4CFB-AB37-7A1653BCC720} = {CA5046EC-C894-4535-8190-A31F75FDEB96} {FE7AB616-97A5-46D4-A8B1-B2980A8C7379} = {CA5046EC-C894-4535-8190-A31F75FDEB96} {A7A02F2B-36E1-46A5-AF1F-E58E99E73324} = {1AD64707-0BE0-48B0-A803-916FF96DCB4F} + {487B9A76-363D-441F-84B6-ADEDEA1F2E39} = {06678E46-B4E2-432B-A46E-56DEEF43F6CE} + {E056C05D-9BAD-4B69-9751-5C26D76319A4} = {06678E46-B4E2-432B-A46E-56DEEF43F6CE} + {046C7454-643D-4C83-A2C7-50E060446B18} = {06678E46-B4E2-432B-A46E-56DEEF43F6CE} + {937DD70A-FF53-4129-A7C5-FD5F0758591D} = {06678E46-B4E2-432B-A46E-56DEEF43F6CE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {20C98D35-54EF-46A6-8F3B-1855C1AE4F70} diff --git a/samples/core/Testing/BloggingWebApi/BloggingWebApi.csproj b/samples/core/Testing/BloggingWebApi/BloggingWebApi.csproj new file mode 100644 index 0000000000..d3cf58c2d5 --- /dev/null +++ b/samples/core/Testing/BloggingWebApi/BloggingWebApi.csproj @@ -0,0 +1,14 @@ + + + + net6.0 + Items + EF.Testing.BloggingWebApi + EF.Testing.BloggingWebApi + + + + + + + diff --git a/samples/core/Testing/BloggingWebApi/Controllers/BloggingController.cs b/samples/core/Testing/BloggingWebApi/Controllers/BloggingController.cs new file mode 100644 index 0000000000..bd62236dc6 --- /dev/null +++ b/samples/core/Testing/BloggingWebApi/Controllers/BloggingController.cs @@ -0,0 +1,59 @@ +using System.Data; +using System.Linq; +using EF.Testing.BusinessLogic; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace EF.Testing.BloggingWebApi.Controllers +{ + [ApiController] + [Route("[controller]")] + public class BloggingController : ControllerBase + { + private readonly BloggingContext _context; + + public BloggingController(BloggingContext context) + => _context = context; + + #region GetBlog + [HttpGet] + public ActionResult GetBlog(string name) + { + var blog = _context.Blogs.FirstOrDefault(b => b.Name == name); + return blog is null ? NotFound() : blog; + } + #endregion + + [HttpGet] + public ActionResult GetAllBlogs() + => _context.Blogs.OrderBy(b => b.Name).ToArray(); + + [HttpPost] + public ActionResult AddBlog(string name, string url) + { + _context.Blogs.Add(new Blog { Name = name, Url = url }); + _context.SaveChanges(); + + return Ok(); + } + + [HttpPost] + public ActionResult UpdateBlogUrl(string name, string url) + { + // Note: it isn't usually necessary to start a transaction for updating. This is done here for illustration purposes only. + using var transaction = _context.Database.BeginTransaction(IsolationLevel.Serializable); + + var blog = _context.Blogs.FirstOrDefault(b => b.Name == name); + if (blog is null) + { + return NotFound(); + } + + blog.Url = url; + _context.SaveChanges(); + + transaction.Commit(); + return Ok(); + } + } +} diff --git a/samples/core/Testing/BloggingWebApi/Controllers/BloggingControllerWithRepository.cs b/samples/core/Testing/BloggingWebApi/Controllers/BloggingControllerWithRepository.cs new file mode 100644 index 0000000000..d07f281b76 --- /dev/null +++ b/samples/core/Testing/BloggingWebApi/Controllers/BloggingControllerWithRepository.cs @@ -0,0 +1,52 @@ +using System; +using System.Data; +using System.Linq; +using EF.Testing.BusinessLogic; +using Microsoft.AspNetCore.Mvc; + +namespace EF.Testing.BloggingWebApi.Controllers +{ + [ApiController] + [Route("[controller]")] + public class BloggingControllerWithRepository : ControllerBase + { + #region BloggingControllerWithRepository + private readonly IBloggingRepository _repository; + + public BloggingControllerWithRepository(IBloggingRepository repository) + => _repository = repository; + + [HttpGet] + public Blog GetBlog(string name) + => _repository.GetBlogByName(name); + #endregion + + [HttpGet] + public ActionResult GetAllBlogs() + => _repository.GetAllBlogs().ToArray(); + + [HttpPost] + public ActionResult AddBlog(string name, string url) + { + _repository.AddBlog(new Blog { Name = name, Url = url }); + _repository.SaveChanges(); + + return Ok(); + } + + [HttpPost] + public ActionResult UpdateBlogUrl(string name, string url) + { + var blog = _repository.GetBlogByName(name); + if (blog is null) + { + return NotFound(); + } + + blog.Url = url; + _repository.SaveChanges(); + + return Ok(); + } + } +} diff --git a/samples/core/Miscellaneous/Testing/ItemsWebApi/ItemsWebApi/Program.cs b/samples/core/Testing/BloggingWebApi/Program.cs similarity index 92% rename from samples/core/Miscellaneous/Testing/ItemsWebApi/ItemsWebApi/Program.cs rename to samples/core/Testing/BloggingWebApi/Program.cs index 9d0056522a..c4aeb6deb5 100644 --- a/samples/core/Miscellaneous/Testing/ItemsWebApi/ItemsWebApi/Program.cs +++ b/samples/core/Testing/BloggingWebApi/Program.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; -namespace Items +namespace EF.Testing.BloggingWebApi { public class Program { diff --git a/samples/core/Miscellaneous/Testing/ItemsWebApi/ItemsWebApi/Startup.cs b/samples/core/Testing/BloggingWebApi/Startup.cs similarity index 80% rename from samples/core/Miscellaneous/Testing/ItemsWebApi/ItemsWebApi/Startup.cs rename to samples/core/Testing/BloggingWebApi/Startup.cs index 99c27ac73e..85b81ae565 100644 --- a/samples/core/Miscellaneous/Testing/ItemsWebApi/ItemsWebApi/Startup.cs +++ b/samples/core/Testing/BloggingWebApi/Startup.cs @@ -1,3 +1,4 @@ +using EF.Testing.BusinessLogic; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; @@ -5,7 +6,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -namespace Items +namespace EF.Testing.BloggingWebApi { public class Startup { @@ -16,17 +17,19 @@ public Startup(IConfiguration configuration) public IConfiguration Configuration { get; } - // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddControllers(); - services.AddDbContext( + services.AddDbContext( b => b.UseSqlServer( @"Server=(localdb)\mssqllocaldb;Database=EFTestSample;Trusted_Connection=True")); + + #region RegisterRepositoryInDI + services.AddScoped(); + #endregion } - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) diff --git a/samples/core/Miscellaneous/Testing/ItemsWebApi/ItemsWebApi/appsettings.Development.json b/samples/core/Testing/BloggingWebApi/appsettings.Development.json similarity index 100% rename from samples/core/Miscellaneous/Testing/ItemsWebApi/ItemsWebApi/appsettings.Development.json rename to samples/core/Testing/BloggingWebApi/appsettings.Development.json diff --git a/samples/core/Miscellaneous/Testing/ItemsWebApi/ItemsWebApi/appsettings.json b/samples/core/Testing/BloggingWebApi/appsettings.json similarity index 100% rename from samples/core/Miscellaneous/Testing/ItemsWebApi/ItemsWebApi/appsettings.json rename to samples/core/Testing/BloggingWebApi/appsettings.json diff --git a/samples/core/Miscellaneous/Testing/BusinessLogic/Blog.cs b/samples/core/Testing/BusinessLogic/Blog.cs similarity index 59% rename from samples/core/Miscellaneous/Testing/BusinessLogic/Blog.cs rename to samples/core/Testing/BusinessLogic/Blog.cs index 5ad226e290..64175083f2 100644 --- a/samples/core/Miscellaneous/Testing/BusinessLogic/Blog.cs +++ b/samples/core/Testing/BusinessLogic/Blog.cs @@ -1,8 +1,9 @@ -namespace BusinessLogic +namespace EF.Testing.BusinessLogic { public class Blog { public int BlogId { get; set; } + public string Name { get; set; } public string Url { get; set; } } } diff --git a/samples/core/Testing/BusinessLogic/BloggingContext.cs b/samples/core/Testing/BusinessLogic/BloggingContext.cs new file mode 100644 index 0000000000..f8bc95a8f8 --- /dev/null +++ b/samples/core/Testing/BusinessLogic/BloggingContext.cs @@ -0,0 +1,33 @@ +using System; +using Microsoft.EntityFrameworkCore; + +namespace EF.Testing.BusinessLogic +{ + public class BloggingContext : DbContext + { + #region Constructors + public BloggingContext() + { + } + + public BloggingContext(DbContextOptions options) + : base(options) + { + } + + #endregion + + public DbSet Blogs => Set(); + + #region OnConfiguring + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + if (!optionsBuilder.IsConfigured) + { + optionsBuilder.UseSqlServer( + @"Server=(localdb)\mssqllocaldb;Database=EFProviders.InMemory;Trusted_Connection=True"); + } + } + #endregion + } +} diff --git a/samples/core/Testing/BusinessLogic/BloggingRepository.cs b/samples/core/Testing/BusinessLogic/BloggingRepository.cs new file mode 100644 index 0000000000..5bf52d05ff --- /dev/null +++ b/samples/core/Testing/BusinessLogic/BloggingRepository.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Linq; + +namespace EF.Testing.BusinessLogic +{ + #region BloggingRepository + public class BloggingRepository : IBloggingRepository + { + private readonly BloggingContext _context; + + public BloggingRepository(BloggingContext context) + => _context = context; + + public Blog GetBlogByName(string name) + => _context.Blogs.FirstOrDefault(b => b.Name == name); + #endregion + + public IEnumerable GetAllBlogs() + => _context.Blogs; + + public void AddBlog(Blog blog) + => _context.Add(blog); + + public void SaveChanges() + => _context.SaveChanges(); + } +} diff --git a/samples/core/Miscellaneous/Testing/BusinessLogic/BusinessLogic.csproj b/samples/core/Testing/BusinessLogic/BusinessLogic.csproj similarity index 68% rename from samples/core/Miscellaneous/Testing/BusinessLogic/BusinessLogic.csproj rename to samples/core/Testing/BusinessLogic/BusinessLogic.csproj index 3c3e36c369..00665806a1 100644 --- a/samples/core/Miscellaneous/Testing/BusinessLogic/BusinessLogic.csproj +++ b/samples/core/Testing/BusinessLogic/BusinessLogic.csproj @@ -2,8 +2,8 @@ net6.0 - EFTesting.BusinessLogic - EFTesting.BusinessLogic + EF.Testing.BusinessLogic + EF.Testing.BusinessLogic diff --git a/samples/core/Testing/BusinessLogic/IBloggingRepository.cs b/samples/core/Testing/BusinessLogic/IBloggingRepository.cs new file mode 100644 index 0000000000..c10b36610b --- /dev/null +++ b/samples/core/Testing/BusinessLogic/IBloggingRepository.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace EF.Testing.BusinessLogic +{ + #region IBloggingRepository + public interface IBloggingRepository + { + Blog GetBlogByName(string name); + + IEnumerable GetAllBlogs(); + + void AddBlog(Blog blog); + + void SaveChanges(); + } + #endregion +} diff --git a/samples/core/Testing/TestingWithTheDatabase/BloggingControllerTest.cs b/samples/core/Testing/TestingWithTheDatabase/BloggingControllerTest.cs new file mode 100644 index 0000000000..777983a083 --- /dev/null +++ b/samples/core/Testing/TestingWithTheDatabase/BloggingControllerTest.cs @@ -0,0 +1,63 @@ +using System.Linq; +using EF.Testing.BloggingWebApi.Controllers; +using Xunit; + +namespace EF.Testing.IntegrationTests +{ + #region UsingTheFixture + public class BloggingControllerTest : IClassFixture + { + public BloggingControllerTest(TestDatabaseFixture fixture) + => Fixture = fixture; + + public TestDatabaseFixture Fixture { get; } + #endregion + + #region GetBlog + [Fact] + public void GetBlog() + { + using var context = Fixture.CreateContext(); + var controller = new BloggingController(context); + + var blog = controller.GetBlog("Blog2").Value; + + Assert.Equal("http://blog2.com", blog.Url); + } + #endregion + + #region GetAllBlogs + [Fact] + public void GetAllBlogs() + { + using var context = Fixture.CreateContext(); + var controller = new BloggingController(context); + + var blogs = controller.GetAllBlogs().Value; + + Assert.Collection( + blogs, + b => Assert.Equal("Blog1", b.Name), + b => Assert.Equal("Blog2", b.Name)); + } + #endregion + + #region AddBlog + [Fact] + public void AddBlog() + { + using var context = Fixture.CreateContext(); + context.Database.BeginTransaction(); + + var controller = new BloggingController(context); + controller.AddBlog("Blog3", "http://blog3.com"); + + context.ChangeTracker.Clear(); + + var blog = context.Blogs.Single(b => b.Name == "Blog3"); + Assert.Equal("http://blog3.com", blog.Url); + + } + #endregion + } +} diff --git a/samples/core/Testing/TestingWithTheDatabase/TestDatabaseFixture.cs b/samples/core/Testing/TestingWithTheDatabase/TestDatabaseFixture.cs new file mode 100644 index 0000000000..c61aab63c7 --- /dev/null +++ b/samples/core/Testing/TestingWithTheDatabase/TestDatabaseFixture.cs @@ -0,0 +1,43 @@ +using EF.Testing.BusinessLogic; +using Microsoft.EntityFrameworkCore; + +namespace EF.Testing.IntegrationTests +{ + #region TestDatabaseFixture + public class TestDatabaseFixture + { + private const string ConnectionString = @"Server=(localdb)\mssqllocaldb;Database=EFTestSample;Trusted_Connection=True"; + + private static readonly object _lock = new(); + private static bool _databaseInitialized; + + public TestDatabaseFixture() + { + lock (_lock) + { + if (!_databaseInitialized) + { + using (var context = CreateContext()) + { + context.Database.EnsureDeleted(); + context.Database.EnsureCreated(); + + context.AddRange( + new Blog { Name = "Blog1", Url = "http://blog1.com" }, + new Blog { Name = "Blog2", Url = "http://blog2.com" }); + context.SaveChanges(); + } + + _databaseInitialized = true; + } + } + } + + public BloggingContext CreateContext() + => new BloggingContext( + new DbContextOptionsBuilder() + .UseSqlServer(ConnectionString) + .Options); + } + #endregion +} diff --git a/samples/core/Testing/TestingWithTheDatabase/TestingWithTheDatabase.csproj b/samples/core/Testing/TestingWithTheDatabase/TestingWithTheDatabase.csproj new file mode 100644 index 0000000000..412ba53fe2 --- /dev/null +++ b/samples/core/Testing/TestingWithTheDatabase/TestingWithTheDatabase.csproj @@ -0,0 +1,23 @@ + + + + net6.0 + EF.Testing.IntegrationTests + EF.Testing.IntegrationTests + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/samples/core/Testing/TestingWithTheDatabase/TransactionalBloggingControllerTest.cs b/samples/core/Testing/TestingWithTheDatabase/TransactionalBloggingControllerTest.cs new file mode 100644 index 0000000000..d86fa3b24a --- /dev/null +++ b/samples/core/Testing/TestingWithTheDatabase/TransactionalBloggingControllerTest.cs @@ -0,0 +1,41 @@ +using System; +using System.Linq; +using EF.Testing.BloggingWebApi.Controllers; +using Xunit; + +namespace EF.Testing.IntegrationTests +{ + #region UsingTheFixture + [Collection("TransactionalTests")] + public class TransactionalBloggingControllerTest : IDisposable + { + public TransactionalBloggingControllerTest(TransactionalTestDatabaseFixture fixture) + => Fixture = fixture; + + public TransactionalTestDatabaseFixture Fixture { get; } + #endregion + + #region UpdateBlogUrl + [Fact] + public void UpdateBlogUrl() + { + using (var context = Fixture.CreateContext()) + { + var controller = new BloggingController(context); + controller.UpdateBlogUrl("Blog2", "http://blog2_updated.com"); + } + + using (var context = Fixture.CreateContext()) + { + var blog = context.Blogs.Single(b => b.Name == "Blog2"); + Assert.Equal("http://blog2_updated.com", blog.Url); + } + } + #endregion + + #region Dispose + public void Dispose() + => Fixture.Cleanup(); + #endregion + } +} diff --git a/samples/core/Testing/TestingWithTheDatabase/TransactionalTestDatabaseFixture.cs b/samples/core/Testing/TestingWithTheDatabase/TransactionalTestDatabaseFixture.cs new file mode 100644 index 0000000000..dcd6ee6f8d --- /dev/null +++ b/samples/core/Testing/TestingWithTheDatabase/TransactionalTestDatabaseFixture.cs @@ -0,0 +1,49 @@ +using EF.Testing.BusinessLogic; +using Microsoft.EntityFrameworkCore; +using Xunit; + +namespace EF.Testing.IntegrationTests +{ + #region TransactionalTestDatabaseFixture + public class TransactionalTestDatabaseFixture + { + private const string ConnectionString = @"Server=(localdb)\mssqllocaldb;Database=EFTransactionalTestSample;Trusted_Connection=True"; + + public BloggingContext CreateContext() + => new BloggingContext( + new DbContextOptionsBuilder() + .UseSqlServer(ConnectionString) + .Options); + + public TransactionalTestDatabaseFixture() + { + using var context = CreateContext(); + context.Database.EnsureDeleted(); + context.Database.EnsureCreated(); + + Cleanup(); + } + + public void Cleanup() + { + #region Cleanup + using var context = CreateContext(); + + context.Blogs.RemoveRange(context.Blogs); + + context.AddRange( + new Blog { Name = "Blog1", Url = "http://blog1.com" }, + new Blog { Name = "Blog2", Url = "http://blog2.com" }); + context.SaveChanges(); + #endregion + } + } + #endregion + + #region CollectionDefinition + [CollectionDefinition("TransactionalTests")] + public class TransactionalTestsCollection : ICollectionFixture + { + } + #endregion +} diff --git a/samples/core/Testing/TestingWithoutTheDatabase/InMemoryBloggingControllerTest.cs b/samples/core/Testing/TestingWithoutTheDatabase/InMemoryBloggingControllerTest.cs new file mode 100644 index 0000000000..3234e855e6 --- /dev/null +++ b/samples/core/Testing/TestingWithoutTheDatabase/InMemoryBloggingControllerTest.cs @@ -0,0 +1,88 @@ +using System.Linq; +using EF.Testing.BloggingWebApi.Controllers; +using EF.Testing.BusinessLogic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Xunit; + +namespace EF.Testing.UnitTests +{ + public class InMemoryBloggingControllerTest + { + private readonly DbContextOptions _contextOptions; + + #region Constructor + public InMemoryBloggingControllerTest() + { + _contextOptions = new DbContextOptionsBuilder() + .UseInMemoryDatabase("BloggingControllerTest") + .ConfigureWarnings(b => b.Ignore(InMemoryEventId.TransactionIgnoredWarning)) + .Options; + + using var context = new BloggingContext(_contextOptions); + + context.Database.EnsureDeleted(); + context.Database.EnsureCreated(); + + context.AddRange( + new Blog { Name = "Blog1", Url = "http://blog1.com" }, + new Blog { Name = "Blog2", Url = "http://blog2.com" }); + + context.SaveChanges(); + } + #endregion + + #region GetBlog + [Fact] + public void GetBlog() + { + using var context = CreateContext(); + var controller = new BloggingController(context); + + var blog = controller.GetBlog("Blog2").Value; + + Assert.Equal("http://blog2.com", blog.Url); + } + #endregion + + [Fact] + public void GetAllBlogs() + { + using var context = CreateContext(); + var controller = new BloggingController(context); + + var blogs = controller.GetAllBlogs().Value; + + Assert.Collection( + blogs, + b => Assert.Equal("Blog1", b.Name), + b => Assert.Equal("Blog2", b.Name)); + } + + [Fact] + public void AddBlog() + { + using var context = CreateContext(); + var controller = new BloggingController(context); + + controller.AddBlog("Blog3", "http://blog3.com"); + + var blog = context.Blogs.Single(b => b.Name == "Blog3"); + Assert.Equal("http://blog3.com", blog.Url); + } + + [Fact] + public void UpdateBlogUrl() + { + using var context = CreateContext(); + var controller = new BloggingController(context); + + controller.UpdateBlogUrl("Blog2", "http://blog2_updated.com"); + + var blog = context.Blogs.Single(b => b.Name == "Blog2"); + Assert.Equal("http://blog2_updated.com", blog.Url); + } + + BloggingContext CreateContext() => new BloggingContext(_contextOptions); + } +} diff --git a/samples/core/Testing/TestingWithoutTheDatabase/RepositoryBloggingControllerTest.cs b/samples/core/Testing/TestingWithoutTheDatabase/RepositoryBloggingControllerTest.cs new file mode 100644 index 0000000000..b3737da074 --- /dev/null +++ b/samples/core/Testing/TestingWithoutTheDatabase/RepositoryBloggingControllerTest.cs @@ -0,0 +1,92 @@ +using EF.Testing.BloggingWebApi.Controllers; +using EF.Testing.BusinessLogic; +using Moq; +using Xunit; + +namespace EF.Testing.UnitTests +{ + public class RepositoryBloggingControllerTest + { + #region GetBlog + [Fact] + public void GetBlog() + { + // Arrange + var repositoryMock = new Mock(); + repositoryMock + .Setup(r => r.GetBlogByName("Blog2")) + .Returns(new Blog { Name = "Blog2", Url = "http://blog2.com" }); + + var controller = new BloggingControllerWithRepository(repositoryMock.Object); + + // Act + var blog = controller.GetBlog("Blog2"); + + // Assert + repositoryMock.Verify(r => r.GetBlogByName("Blog2")); + Assert.Equal("http://blog2.com", blog.Url); + } + #endregion + + [Fact] + public void GetAllBlogs() + { + // Arrange + var repositoryMock = new Mock(); + repositoryMock + .Setup(r => r.GetAllBlogs()) + .Returns(new[] + { + new Blog { Name = "Blog1", Url = "http://blog1.com" }, + new Blog { Name = "Blog2", Url = "http://blog2.com" } + }); + + var controller = new BloggingControllerWithRepository(repositoryMock.Object); + + // Act + var blogs = controller.GetAllBlogs().Value; + + // Assert + repositoryMock.Verify(r => r.GetAllBlogs()); + Assert.Equal("http://blog1.com", blogs[0].Url); + Assert.Equal("http://blog2.com", blogs[1].Url); + } + + [Fact] + public void AddBlog() + { + // Arrange + var repositoryMock = new Mock(); + var controller = new BloggingControllerWithRepository(repositoryMock.Object); + + // Act + controller.AddBlog("Blog2", "http://blog2.com"); + + // Assert + repositoryMock.Verify(r => r.AddBlog(It.IsAny())); + repositoryMock.Verify(r => r.SaveChanges()); + } + + [Fact] + public void UpdateBlogUrl() + { + var blog = new Blog { Name = "Blog2", Url = "http://blog2.com" }; + + // Arrange + var repositoryMock = new Mock(); + repositoryMock + .Setup(r => r.GetBlogByName("Blog2")) + .Returns(blog); + + var controller = new BloggingControllerWithRepository(repositoryMock.Object); + + // Act + controller.UpdateBlogUrl("Blog2", "http://blog2_updated.com"); + + // Assert + repositoryMock.Verify(r => r.GetBlogByName("Blog2")); + repositoryMock.Verify(r => r.SaveChanges()); + Assert.Equal("http://blog2_updated.com", blog.Url); + } + } +} diff --git a/samples/core/Testing/TestingWithoutTheDatabase/SqliteInMemoryBloggingControllerTest.cs b/samples/core/Testing/TestingWithoutTheDatabase/SqliteInMemoryBloggingControllerTest.cs new file mode 100644 index 0000000000..74ad2d5c8d --- /dev/null +++ b/samples/core/Testing/TestingWithoutTheDatabase/SqliteInMemoryBloggingControllerTest.cs @@ -0,0 +1,97 @@ +using System; +using System.Data.Common; +using System.Linq; +using EF.Testing.BloggingWebApi.Controllers; +using EF.Testing.BusinessLogic; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Xunit; + +namespace EF.Testing.UnitTests +{ + public class SqliteInMemoryBloggingControllerTest : IDisposable + { + private readonly DbConnection _connection; + private readonly DbContextOptions _contextOptions; + + #region ConstructorAndDispose + public SqliteInMemoryBloggingControllerTest() + { + // Create and open a connection. This creates the SQLite in-memory database, which will persist until the connection is closed + // at the end of the test (see Dispose below). + _connection = new SqliteConnection("Filename=:memory:"); + _connection.Open(); + + // These options will be used by the context instances in this test suite, including the connection opened above. + _contextOptions = new DbContextOptionsBuilder() + .UseSqlite(_connection) + .Options; + + // Create the schema and seed some data + using var context = new BloggingContext(_contextOptions); + + context.Database.EnsureCreated(); + + context.AddRange( + new Blog { Name = "Blog1", Url = "http://blog1.com" }, + new Blog { Name = "Blog2", Url = "http://blog2.com" }); + context.SaveChanges(); + } + + BloggingContext CreateContext() => new BloggingContext(_contextOptions); + + public void Dispose() => _connection.Dispose(); + #endregion + + #region GetBlog + [Fact] + public void GetBlog() + { + using var context = CreateContext(); + var controller = new BloggingController(context); + + var blog = controller.GetBlog("Blog2").Value; + + Assert.Equal("http://blog2.com", blog.Url); + } + #endregion + + [Fact] + public void GetAllBlogs() + { + using var context = CreateContext(); + var controller = new BloggingController(context); + + var blogs = controller.GetAllBlogs().Value; + + Assert.Collection( + blogs, + b => Assert.Equal("Blog1", b.Name), + b => Assert.Equal("Blog2", b.Name)); + } + + [Fact] + public void AddBlog() + { + using var context = CreateContext(); + var controller = new BloggingController(context); + + controller.AddBlog("Blog3", "http://blog3.com"); + + var blog = context.Blogs.Single(b => b.Name == "Blog3"); + Assert.Equal("http://blog3.com", blog.Url); + } + + [Fact] + public void UpdateBlogUrl() + { + using var context = CreateContext(); + var controller = new BloggingController(context); + + controller.UpdateBlogUrl("Blog2", "http://blog2_updated.com"); + + var blog = context.Blogs.Single(b => b.Name == "Blog2"); + Assert.Equal("http://blog2_updated.com", blog.Url); + } + } +} diff --git a/samples/core/Testing/TestingWithoutTheDatabase/TestingWithoutTheDatabase.csproj b/samples/core/Testing/TestingWithoutTheDatabase/TestingWithoutTheDatabase.csproj new file mode 100644 index 0000000000..c874712af1 --- /dev/null +++ b/samples/core/Testing/TestingWithoutTheDatabase/TestingWithoutTheDatabase.csproj @@ -0,0 +1,27 @@ + + + + net6.0 + EF.Testing.UnitTests + EF.Testing.UnitTests + false + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + +