diff --git a/docs/Tutorials/AspNetCore-Mvc/Part-I.md b/docs/Tutorials/AspNetCore-Mvc/Part-I.md index 290c0a558e..43fbf7b500 100644 --- a/docs/Tutorials/AspNetCore-Mvc/Part-I.md +++ b/docs/Tutorials/AspNetCore-Mvc/Part-I.md @@ -20,7 +20,7 @@ This tutorial assumes that you have created a new project, named `Acme.BookStore This is the layered solution structure created from the startup template: -![bookstore-visual-studio-solution](../../images/bookstore-visual-studio-solution.png) +![bookstore-visual-studio-solution](images/bookstore-visual-studio-solution.png) ### Create the Book Entity @@ -95,7 +95,7 @@ public class BookStoreDbContext : AbpDbContext Startup template uses [EF Core Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/) to create and maintain the database schema. Open the **Package Manager Console (PMC)**, select the `Acme.BookStore.EntityFrameworkCore` as the **default project** and execute the following command: -![bookstore-pmc-add-book-migration](../../images/bookstore-pmc-add-book-migration.png) +![bookstore-pmc-add-book-migration](images/bookstore-pmc-add-book-migration.png) This will create a new migration class inside the `Migrations` folder. Then execute the `Update-Database` command to update the database schema: @@ -107,7 +107,7 @@ PM> Update-Database `Update-Database` command created the `Books` table in the database. Enter a few sample rows, so you can show them on the page: -![bookstore-books-table](../../images/bookstore-books-table.png) +![bookstore-books-table](images/bookstore-books-table.png) ### Create the Application Service @@ -247,7 +247,7 @@ ABP can automatically configures your application services as MVC API Controller The startup template is configured to run the [swagger UI](https://swagger.io/tools/swagger-ui/) using the [Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) library. Run the application and enter `http://localhost:53929/swagger/` as URL on your browser: -![bookstore-swagger](../../images/bookstore-swagger.png) +![bookstore-swagger](images/bookstore-swagger.png) You will see some built-in service endpoints as well as the `Book` service and its REST-style endpoints. @@ -273,7 +273,7 @@ acme.bookStore.book.getList({}).done(function (result) { console.log(result); }) Running this code produces such an output: -![bookstore-test-js-proxy-getlist](../../images/bookstore-test-js-proxy-getlist.png) +![bookstore-test-js-proxy-getlist](images/bookstore-test-js-proxy-getlist.png) You can see the **book list** returned from the server. @@ -297,7 +297,7 @@ It's time to create something visible! Instead of classic MVC, we will use the n Create a new `Books` folder under the `Pages` folder of the `Acme.BookStore.Web` project and add a new Razor Page named `Index.html`: -![bookstore-add-index-page](../../images/bookstore-add-index-page.png) +![bookstore-add-index-page](images/bookstore-add-index-page.png) Open the `Index.cshtml` and change the content as shown below: @@ -327,7 +327,7 @@ context.Menu.AddItem( Localization texts are located under the `Localization/BookStore` folder of the `Acme.BookStore.Domain` project: -![bookstore-localization-files](../../images/bookstore-localization-files.png) +![bookstore-localization-files](images/bookstore-localization-files.png) Open the `en.json` file and add localization texts for `Menu:BookStore` and `Menu:Books` keys: @@ -347,7 +347,7 @@ Open the `en.json` file and add localization texts for `Menu:BookStore` and `Men Run the application and see the menu items are added to the top bar: -![bookstore-menu-items](../../images/bookstore-menu-items.png) +![bookstore-menu-items](images/bookstore-menu-items.png) When you click to the Books menu item, you are redirected to the new Books page. @@ -396,7 +396,7 @@ Change the `Pages/Books/Index.cshtml` as following: Create `index.js` JavaScript file under the `wwwroot/pages/books/` folder: -![bookstore-index-js-file](../../images/bookstore-index-js-file.png) +![bookstore-index-js-file](images/bookstore-index-js-file.png) `index.js` content is shown below: @@ -436,7 +436,7 @@ $(function() { The final UI is shown below: -![bookstore-book-list](../../images/bookstore-book-list.png) +![bookstore-book-list](images/bookstore-book-list.png) ### Next Part diff --git a/docs/Tutorials/AspNetCore-Mvc/Part-II.md b/docs/Tutorials/AspNetCore-Mvc/Part-II.md index 422e226d15..adcc389e7b 100644 --- a/docs/Tutorials/AspNetCore-Mvc/Part-II.md +++ b/docs/Tutorials/AspNetCore-Mvc/Part-II.md @@ -16,13 +16,13 @@ You can download the **source code** of the application [from here](https://gith In this section, you will learn how to create a new modal dialog form to create a new book. The result dialog will be like that: -![bookstore-create-dialog](../../images/bookstore-create-dialog.png) +![bookstore-create-dialog](images/bookstore-create-dialog.png) #### Create the Modal Form Create a new razor page, named `CreateModal.cshtml` under the `Pages/Books` folder of the `Acme.BookStore.Web` project: -![bookstore-add-create-dialog](../../images/bookstore-add-create-dialog.png) +![bookstore-add-create-dialog](images/bookstore-add-create-dialog.png) ##### CreateModal.cshtml.cs @@ -110,7 +110,7 @@ Open the `Pages/Books/Index.cshtml` and change the `abp-card-header` tag as show Just added a **New book** button to the **top right** of the table: -![bookstore-new-book-button](../../images/bookstore-new-book-button.png) +![bookstore-new-book-button](images/bookstore-new-book-button.png) Open the `wwwroot/pages/books/index.js` and add the following code just after the datatable configuration: @@ -135,7 +135,7 @@ Now, you can **run the application** and add new books using the new modal form. Create a new razor page, named `EditModal.cshtml` under the `Pages/Books` folder of the `Acme.BookStore.Web` project: -![bookstore-add-edit-dialog](../../images/bookstore-add-edit-dialog.png) +![bookstore-add-edit-dialog](images/bookstore-add-edit-dialog.png) #### EditModal.cshtml.cs @@ -251,7 +251,7 @@ This page is very similar to the `CreateModal.cshtml` except; We will add a dropdown button ("Actions") for each row of the table. The final UI looks like this: -![bookstore-books-table-actions](../../images/bookstore-books-table-actions.png) +![bookstore-books-table-actions](images/bookstore-books-table-actions.png) Open the `Pages/Books/Index.cshtml` page and change the table section as shown below: diff --git a/docs/Tutorials/AspNetCore-Mvc/Part-III.md b/docs/Tutorials/AspNetCore-Mvc/Part-III.md index 18e006adf4..1b03bc8059 100644 --- a/docs/Tutorials/AspNetCore-Mvc/Part-III.md +++ b/docs/Tutorials/AspNetCore-Mvc/Part-III.md @@ -12,4 +12,207 @@ This is the third part of the tutorial series. See all parts: You can download the **source code** of the application [from here](https://github.com/volosoft/abp/tree/master/samples/BookStore). -TODO... \ No newline at end of file +### Test Projects in the Solution + +There are two test projects in the solution: + +![bookstore-test-projects](images/bookstore-test-projects.png) + +* `Acme.BookStore.Application.Tests` is for unit & integration test projects. You can write tests for application services those are integrated to the framework. It uses **EF Core SQLite in-memory** database. +* `Acme.BookStore.Web.Tests` is for full stack integration tests including the web layer. So, you can write tests for UI too. + +Test projects uses the following libraries for testing: + +* [xunit](https://xunit.github.io/) as the main test framework. +* [Shoudly](http://shouldly.readthedocs.io/en/latest/) as an assertion library. +* [NSubstitute](http://nsubstitute.github.io/) as a mocking library. + +### Adding Test Data + +Startup template contains the `BookStoreTestDataBuilder` class in the `Acme.BookStore.Application.Tests` project that creates some data to run tests on. It's shown below: + +````C# +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Identity; +using Volo.Abp.Threading; + +namespace Acme.BookStore +{ + public class BookStoreTestDataBuilder : ITransientDependency + { + private readonly IIdentityDataSeeder _identityDataSeeder; + + public BookStoreTestDataBuilder(IIdentityDataSeeder identityDataSeeder) + { + _identityDataSeeder = identityDataSeeder; + } + + public void Build() + { + AsyncHelper.RunSync(BuildInternalAsync); + } + + public async Task BuildInternalAsync() + { + await _identityDataSeeder.SeedAsync("1q2w3E*"); + } + } +} +```` + +* It simply uses `IIdentityDataSeeder` which is implemented by the identity module and creates an admin role and admin user. You can use them in the tests. + +Change the `BookStoreTestDataBuilder` class as show below: + +````C# +using System; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Identity; +using Volo.Abp.Threading; + +namespace Acme.BookStore +{ + public class BookStoreTestDataBuilder : ITransientDependency + { + private readonly IIdentityDataSeeder _identityDataSeeder; + private readonly IRepository _bookRepository; + + public BookStoreTestDataBuilder( + IIdentityDataSeeder identityDataSeeder, + IRepository bookRepository) + { + _identityDataSeeder = identityDataSeeder; + _bookRepository = bookRepository; + } + + public void Build() + { + AsyncHelper.RunSync(BuildInternalAsync); + } + + public async Task BuildInternalAsync() + { + await _identityDataSeeder.SeedAsync("1q2w3E*"); + + await _bookRepository.InsertAsync( + new Book + { + Id = Guid.NewGuid(), + Name = "Test book 1", + Type = BookType.Fantastic, + PublishDate = new DateTime(2015, 05, 24), + Price = 21 + } + ); + + await _bookRepository.InsertAsync( + new Book + { + Id = Guid.NewGuid(), + Name = "Test book 2", + Type = BookType.Science, + PublishDate = new DateTime(2014, 02, 11), + Price = 15 + } + ); + } + } +} +```` + +* Injected `IRepository` and used it in the `BuildInternalAsync` to create 2 book entities. + +### Testing the BookAppService + +Create a test class named `BookAppService_Tests` in the `Acme.BookStore.Application.Tests` project: + +````C# +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.Application.Dtos; +using Xunit; + +namespace Acme.BookStore +{ + public class BookAppService_Tests : BookStoreApplicationTestBase + { + private readonly IBookAppService _bookAppService; + + public BookAppService_Tests() + { + _bookAppService = GetRequiredService(); + } + + [Fact] + public async Task Should_Get_List_Of_Books() + { + //Act + var result = await _bookAppService.GetListAsync( + new PagedAndSortedResultRequestDto() + ); + + //Assert + result.TotalCount.ShouldBeGreaterThan(0); + result.Items.ShouldContain(b => b.Name == "Test book 1"); + } + } +} +```` + +* `Should_Get_List_Of_Books` test simply uses `BookAppService.GetListAsync` method to get and check the list of users. + +Add a new test that creates a valid new book: + +````C# +[Fact] +public async Task Should_Create_A_Valid_Book() +{ + //Act + var result = await _bookAppService.CreateAsync( + new CreateUpdateBookDto + { + Name = "New test book 42", + Price = 10, + PublishDate = DateTime.Now, + Type = BookType.ScienceFiction + } + ); + + //Assert + result.Id.ShouldNotBe(Guid.Empty); + result.Name.ShouldBe("New test book 42"); +} +```` + +Add a new test that tries to create an invalid book and fails: + +````C# +[Fact] +public async Task Should_Not_Create_A_Book_Without_Name() +{ + var exception = await Assert.ThrowsAsync(async () => + { + await _bookAppService.CreateAsync( + new CreateUpdateBookDto + { + Name = "", + Price = 10, + PublishDate = DateTime.Now, + Type = BookType.ScienceFiction + } + ); + }); + + exception.ValidationErrors + .ShouldContain(err => err.MemberNames.Any(mem => mem == "Name")); +} +```` + +* Since the `Name` is set as empty, ABP throws an `AbpValidationException`. + +### Testing Web Pages + +TODO \ No newline at end of file diff --git a/docs/images/bookstore-add-create-dialog.png b/docs/Tutorials/AspNetCore-Mvc/images/bookstore-add-create-dialog.png similarity index 100% rename from docs/images/bookstore-add-create-dialog.png rename to docs/Tutorials/AspNetCore-Mvc/images/bookstore-add-create-dialog.png diff --git a/docs/images/bookstore-add-edit-dialog.png b/docs/Tutorials/AspNetCore-Mvc/images/bookstore-add-edit-dialog.png similarity index 100% rename from docs/images/bookstore-add-edit-dialog.png rename to docs/Tutorials/AspNetCore-Mvc/images/bookstore-add-edit-dialog.png diff --git a/docs/images/bookstore-add-index-page.png b/docs/Tutorials/AspNetCore-Mvc/images/bookstore-add-index-page.png similarity index 100% rename from docs/images/bookstore-add-index-page.png rename to docs/Tutorials/AspNetCore-Mvc/images/bookstore-add-index-page.png diff --git a/docs/images/bookstore-book-list.png b/docs/Tutorials/AspNetCore-Mvc/images/bookstore-book-list.png similarity index 100% rename from docs/images/bookstore-book-list.png rename to docs/Tutorials/AspNetCore-Mvc/images/bookstore-book-list.png diff --git a/docs/images/bookstore-books-table-actions.png b/docs/Tutorials/AspNetCore-Mvc/images/bookstore-books-table-actions.png similarity index 100% rename from docs/images/bookstore-books-table-actions.png rename to docs/Tutorials/AspNetCore-Mvc/images/bookstore-books-table-actions.png diff --git a/docs/images/bookstore-books-table.png b/docs/Tutorials/AspNetCore-Mvc/images/bookstore-books-table.png similarity index 100% rename from docs/images/bookstore-books-table.png rename to docs/Tutorials/AspNetCore-Mvc/images/bookstore-books-table.png diff --git a/docs/images/bookstore-create-dialog.png b/docs/Tutorials/AspNetCore-Mvc/images/bookstore-create-dialog.png similarity index 100% rename from docs/images/bookstore-create-dialog.png rename to docs/Tutorials/AspNetCore-Mvc/images/bookstore-create-dialog.png diff --git a/docs/images/bookstore-create-template.png b/docs/Tutorials/AspNetCore-Mvc/images/bookstore-create-template.png similarity index 100% rename from docs/images/bookstore-create-template.png rename to docs/Tutorials/AspNetCore-Mvc/images/bookstore-create-template.png diff --git a/docs/images/bookstore-homepage.png b/docs/Tutorials/AspNetCore-Mvc/images/bookstore-homepage.png similarity index 100% rename from docs/images/bookstore-homepage.png rename to docs/Tutorials/AspNetCore-Mvc/images/bookstore-homepage.png diff --git a/docs/images/bookstore-index-js-file.png b/docs/Tutorials/AspNetCore-Mvc/images/bookstore-index-js-file.png similarity index 100% rename from docs/images/bookstore-index-js-file.png rename to docs/Tutorials/AspNetCore-Mvc/images/bookstore-index-js-file.png diff --git a/docs/images/bookstore-localization-files.png b/docs/Tutorials/AspNetCore-Mvc/images/bookstore-localization-files.png similarity index 100% rename from docs/images/bookstore-localization-files.png rename to docs/Tutorials/AspNetCore-Mvc/images/bookstore-localization-files.png diff --git a/docs/images/bookstore-menu-items.png b/docs/Tutorials/AspNetCore-Mvc/images/bookstore-menu-items.png similarity index 100% rename from docs/images/bookstore-menu-items.png rename to docs/Tutorials/AspNetCore-Mvc/images/bookstore-menu-items.png diff --git a/docs/images/bookstore-new-book-button.png b/docs/Tutorials/AspNetCore-Mvc/images/bookstore-new-book-button.png similarity index 100% rename from docs/images/bookstore-new-book-button.png rename to docs/Tutorials/AspNetCore-Mvc/images/bookstore-new-book-button.png diff --git a/docs/images/bookstore-pmc-add-book-migration.png b/docs/Tutorials/AspNetCore-Mvc/images/bookstore-pmc-add-book-migration.png similarity index 100% rename from docs/images/bookstore-pmc-add-book-migration.png rename to docs/Tutorials/AspNetCore-Mvc/images/bookstore-pmc-add-book-migration.png diff --git a/docs/images/bookstore-swagger.png b/docs/Tutorials/AspNetCore-Mvc/images/bookstore-swagger.png similarity index 100% rename from docs/images/bookstore-swagger.png rename to docs/Tutorials/AspNetCore-Mvc/images/bookstore-swagger.png diff --git a/docs/images/bookstore-test-js-proxy-getlist.png b/docs/Tutorials/AspNetCore-Mvc/images/bookstore-test-js-proxy-getlist.png similarity index 100% rename from docs/images/bookstore-test-js-proxy-getlist.png rename to docs/Tutorials/AspNetCore-Mvc/images/bookstore-test-js-proxy-getlist.png diff --git a/docs/Tutorials/AspNetCore-Mvc/images/bookstore-test-projects.png b/docs/Tutorials/AspNetCore-Mvc/images/bookstore-test-projects.png new file mode 100644 index 0000000000..f9511d7b11 Binary files /dev/null and b/docs/Tutorials/AspNetCore-Mvc/images/bookstore-test-projects.png differ diff --git a/docs/images/bookstore-user-management.png b/docs/Tutorials/AspNetCore-Mvc/images/bookstore-user-management.png similarity index 100% rename from docs/images/bookstore-user-management.png rename to docs/Tutorials/AspNetCore-Mvc/images/bookstore-user-management.png diff --git a/docs/images/bookstore-visual-studio-solution.png b/docs/Tutorials/AspNetCore-Mvc/images/bookstore-visual-studio-solution.png similarity index 100% rename from docs/images/bookstore-visual-studio-solution.png rename to docs/Tutorials/AspNetCore-Mvc/images/bookstore-visual-studio-solution.png diff --git a/samples/BookStore/test/Acme.BookStore.Application.Tests/BookAppService_Tests.cs b/samples/BookStore/test/Acme.BookStore.Application.Tests/BookAppService_Tests.cs new file mode 100644 index 0000000000..78d291bb33 --- /dev/null +++ b/samples/BookStore/test/Acme.BookStore.Application.Tests/BookAppService_Tests.cs @@ -0,0 +1,70 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Validation; +using Xunit; + +namespace Acme.BookStore +{ + public class BookAppService_Tests : BookStoreApplicationTestBase + { + private readonly IBookAppService _bookAppService; + + public BookAppService_Tests() + { + _bookAppService = GetRequiredService(); + } + + [Fact] + public async Task Should_Get_List_Of_Books() + { + //Act + var result = await _bookAppService.GetListAsync(new PagedAndSortedResultRequestDto()); + + //Assert + result.TotalCount.ShouldBeGreaterThan(0); + result.Items.ShouldContain(b => b.Name == "Test book 1"); + } + + [Fact] + public async Task Should_Create_A_Valid_Book() + { + //Act + var result = await _bookAppService.CreateAsync( + new CreateUpdateBookDto + { + Name = "New test book 42", + Price = 10, + PublishDate = DateTime.Now, + Type = BookType.ScienceFiction + } + ); + + //Assert + result.Id.ShouldNotBe(Guid.Empty); + result.Name.ShouldBe("New test book 42"); + } + + [Fact] + public async Task Should_Not_Create_A_Book_Without_Name() + { + var exception = await Assert.ThrowsAsync(async () => + { + await _bookAppService.CreateAsync( + new CreateUpdateBookDto + { + Name = "", + Price = 10, + PublishDate = DateTime.Now, + Type = BookType.ScienceFiction + } + ); + }); + + exception.ValidationErrors + .ShouldContain(err => err.MemberNames.Any(mem => mem == "Name")); + } + } +} diff --git a/samples/BookStore/test/Acme.BookStore.Application.Tests/BookStoreTestDataBuilder.cs b/samples/BookStore/test/Acme.BookStore.Application.Tests/BookStoreTestDataBuilder.cs index 9881b7e7be..d29cad952f 100644 --- a/samples/BookStore/test/Acme.BookStore.Application.Tests/BookStoreTestDataBuilder.cs +++ b/samples/BookStore/test/Acme.BookStore.Application.Tests/BookStoreTestDataBuilder.cs @@ -1,5 +1,7 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; using Volo.Abp.Identity; using Volo.Abp.Threading; @@ -8,10 +10,14 @@ namespace Acme.BookStore public class BookStoreTestDataBuilder : ITransientDependency { private readonly IIdentityDataSeeder _identityDataSeeder; + private readonly IRepository _bookRepository; - public BookStoreTestDataBuilder(IIdentityDataSeeder identityDataSeeder) + public BookStoreTestDataBuilder( + IIdentityDataSeeder identityDataSeeder, + IRepository bookRepository) { _identityDataSeeder = identityDataSeeder; + _bookRepository = bookRepository; } public void Build() @@ -22,6 +28,28 @@ namespace Acme.BookStore public async Task BuildInternalAsync() { await _identityDataSeeder.SeedAsync("1q2w3E*"); + + await _bookRepository.InsertAsync( + new Book + { + Id = Guid.NewGuid(), + Name = "Test book 1", + Type = BookType.Fantastic, + PublishDate = new DateTime(2015, 05, 24), + Price = 21 + } + ); + + await _bookRepository.InsertAsync( + new Book + { + Id = Guid.NewGuid(), + Name = "Test book 2", + Type = BookType.Science, + PublishDate = new DateTime(2014, 02, 11), + Price = 15 + } + ); } } } \ No newline at end of file