From f125b17eaf1ca068a283c15576e91b2e313ff250 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Thu, 15 Aug 2019 10:52:59 +0300 Subject: [PATCH] Revised Angular tutorial Part II --- docs/en/Tutorials/Angular/Part-I.md | 1 + docs/en/Tutorials/Angular/Part-II.md | 176 ++++++++--------- docs/en/Tutorials/Angular/Part-III.md | 178 ++++++++++++++++++ .../images/bookstore-test-projects-v2.png | Bin 0 -> 6864 bytes 4 files changed, 262 insertions(+), 93 deletions(-) create mode 100644 docs/en/Tutorials/Angular/Part-III.md create mode 100644 docs/en/Tutorials/Angular/images/bookstore-test-projects-v2.png diff --git a/docs/en/Tutorials/Angular/Part-I.md b/docs/en/Tutorials/Angular/Part-I.md index fdfddef683..6dadc24841 100644 --- a/docs/en/Tutorials/Angular/Part-I.md +++ b/docs/en/Tutorials/Angular/Part-I.md @@ -8,6 +8,7 @@ This is the first part of the Angular tutorial series. See all parts: - **Part I: Create the project and a book list page (this tutorial)** - [Part II: Create, Update and Delete books](Part-II.md) +- [Part III: Integration Tests](Part-III.md) You can access to the **source code** of the application from the [GitHub repository](https://github.com/abpframework/abp/tree/dev/samples/BookStore-Angular-MongoDb). diff --git a/docs/en/Tutorials/Angular/Part-II.md b/docs/en/Tutorials/Angular/Part-II.md index b322783db3..f18ec9dce1 100644 --- a/docs/en/Tutorials/Angular/Part-II.md +++ b/docs/en/Tutorials/Angular/Part-II.md @@ -6,6 +6,7 @@ This is the second part of the Angular tutorial series. See all parts: - [Part I: Create the project and a book list page](Part-I.md) - **Part II: Create, Update and Delete books (this tutorial)** +- [Part III: Integration Tests](Part-III.md) You can access to the **source code** of the application from the [GitHub repository](https://github.com/abpframework/abp/tree/dev/samples/BookStore-Angular-MongoDb). @@ -37,19 +38,19 @@ Open the `books.service.ts` and add a new method, named `create` to perform an H ```typescript create(body: Books.CreateUpdateBookInput): Observable { - const request: Rest.Request = { + return this.rest.request({ method: 'POST', url: '/api/app/book', body, - }; - - return this.rest.request(request); + }); } ``` +* `rest.request` function gets generic parameters for the types sent to and received from the server. This example sends a `CreateUpdateBookInput` object and receives a `Book` object (you can set `void` for request or return type if not used). + #### State Definitions -Add the `CreateUpdateBook` action to `books.actions.ts` as shown below: +Add the `CreateUpdateBook` action to the `books.actions.ts` as shown below: ```typescript import { Books } from '../models'; @@ -78,7 +79,7 @@ When the `SaveBook` action dispatched, the save method is executed. It call `cre #### Add a Modal to BookListComponent -Open the `book-list.component.html` and add the `abp-modal` to show/hide the book form. +Open the `book-list.component.html` and add the `abp-modal` to show/hide the modal to create a new book. ```html @@ -98,7 +99,7 @@ Open the `book-list.component.html` and add the `abp-modal` to show/hide the boo `abp-modal` is a pre-built component to show modals. While you could use another approach to show a modal, `abp-modal` provides additional benefits. -Add a button, labeled `New book` to show the modal. +Add a button, labeled `New book` to show the modal: ```html
@@ -161,8 +162,8 @@ buildForm() { } ``` -- The `group` method of `FormBuilder` creates a `FormGroup`. -- Added `Validators.required` static method that validation of form element. +- The `group` method of `FormBuilder` (`fb`) creates a `FormGroup`. +- Added `Validators.required` static method that validates the related form element. Modify the `onAdd` method as shown below: @@ -173,7 +174,7 @@ onAdd() { } ``` -### Create the DOM Elements of the Form +#### Create the DOM Elements of the Form Open `book-list.component.html` and add the form in the body template of the modal. @@ -213,28 +214,32 @@ Open `book-list.component.html` and add the form in the body template of the mod ``` -TODO: Add a short description. +* This template creates a form with Name, Price, Type and Publish date fields. > We've used [NgBootstrap datepicker](https://ng-bootstrap.github.io/#/components/datepicker/overview) in this component. -Open the `book-list.component.ts` and then add the `bookTypes`. +Open the `book-list.component.ts` and then create an array, named `bookTypeArr`: ```typescript //... form: FormGroup; -bookTypeArr = Object.keys(Books.BookType).filter(bookType => typeof this.booksType[bookType] === 'number'); +bookTypeArr = Object.keys(Books.BookType).filter( + bookType => typeof this.booksType[bookType] === 'number' +); ``` -The `bookTypes` variable added to generate array from `BookType` enum. The `bookTypes` equals like this: +The `bookTypeArr` contains the fields of the `BookType` enum. Resulting array is shown below: ```js ['Adventure', 'Biography', 'Dystopia', 'Fantastic' ...] ``` -### Add the Datepicker Requirements +This array was used in the previous form template (in the `ngFor` loop). -Import `NgbDatepickerModule` to the `books.module.ts`. +#### Datepicker Requirements + +You need to import `NgbDatepickerModule` to the `books.module.ts`: ```typescript import { NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap'; @@ -248,25 +253,39 @@ import { NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap'; export class BooksModule {} ``` -Open the `book-list.component.html` and then add `providers` as shown below: +Then open the `book-list.component.html` and add `providers` as shown below: ```typescript @Component({ // ... - styleUrls: ['./book-list.component.scss'], providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], }) export class BookListComponent implements OnInit { // ... ``` -> The `NgbDateAdapter` convert Datepicker value type to `Date` type. See the [datepicker adapters](https://ng-bootstrap.github.io/#/components/datepicker/overview) for more details. +> The `NgbDateAdapter` converts Datepicker value to `Date` type. See the [datepicker adapters](https://ng-bootstrap.github.io/#/components/datepicker/overview) for more details. ![new-book-form](images/bookstore-new-book-form.png) -### Create a New Book +#### Saving the Book -Add the `save` method to `BookListComponent` +Open the `book-list.component.html` and add an `abp-button` to save the form. + +```html + + + Save + +``` + +This adds a save button to the bottom area of the modal: + +![bookstore-new-book-form-v2](images/bookstore-new-book-form-v2.png) + +Then define a `save` method in the `BookListComponent`: ```typescript save() { @@ -281,56 +300,32 @@ save() { } ``` -TODO: description ?? +### Updating An Existing Book -Then, open the `book-list.component.html` and add the `abp-button` for the run `save` method. - -```html - - - Save - -``` - -Now, You can add a new book. - -![bookstore-new-book-form-v2](images/bookstore-new-book-form-v2.png) - -### Add HTTP GET and PUT Methods - -TODO: Description +#### BooksService Open the `books.service.ts` and then add the `getById` and `update` methods. ```typescript getById(id: string): Observable { - const request: Rest.Request = { + return this.rest.request({ method: 'GET', url: `/api/app/book/${id}`, - }; - - return this.rest.request(request); + }); } update(body: Books.CreateUpdateBookInput, id: string): Observable { - const request: Rest.Request = { + return this.rest.request({ method: 'PUT', url: `/api/app/book/${id}`, body, - }; - - return this.rest.request(request); + }); } ``` -- Added the `getById` method to get the editing book by performing an HTTP request to the related endpoint. -- Added the `update` method to update a book with the `id` by performing an HTTP request to the related endpoint. - -TODO: header ?? +#### CreateUpdateBook Action -Open the `books.actins.ts` and add `id` parameter. +Open the `books.actins.ts` and add `id` parameter to the `CreateUpdateBook` action: ```typescript export class CreateUpdateBook { @@ -339,9 +334,7 @@ export class CreateUpdateBook { } ``` -Added `id` parameter to reuse the `BooksSave` while updating and creating a book. - -Open `books.state.ts` and then modify the `save` method as show below: +Open `books.state.ts` and modify the `save` method as show below: ```typescript @Action(CreateUpdateBook) @@ -358,9 +351,9 @@ save({ dispatch }: StateContext, { payload, id }: CreateUpdateBook) } ``` -### Update a Book +#### BookListComponent -Inject `BooksService` dependency by adding it to the `book-list.component.ts` constructor and add variable named `selectedBook`. +Inject `BooksService` dependency by adding it to the `book-list.component.ts` constructor and add a variable named `selectedBook`. ```typescript import { BooksService } from '../shared/books.service'; @@ -373,10 +366,7 @@ constructor( ) ``` -- Added `booksService` to get the detail of selected book by `id` before creating the form -- Added `selectedBook` variable to reuse detail of selected book. - -Modify the `buildForm` method to reuse the same form while editing a book. +`booksService` is used to get the editing book to prepare the form. Modify the `buildForm` method to reuse the same form while editing a book. ```typescript buildForm() { @@ -401,9 +391,9 @@ Add the `onEdit` method as shown below: } ``` -- Added `onEdit` method to get selected book detail, build form and then show the modal. +Added `onEdit` method to get the editing book, build the form and show the modal. -Add the `selectedBook` definition to `onAdd` method for reuse same form while adding a new book. +Now, add the `selectedBook` definition to `onAdd` method to reuse the same form while creating a new book: ```typescript onAdd() { @@ -412,7 +402,7 @@ Add the `selectedBook` definition to `onAdd` method for reuse same form while ad } ``` -Modify the `save` method as shown below: +Modify the `save` method to pass the id of the selected book as shown below: ```typescript save() { @@ -428,8 +418,6 @@ save() { } ``` -Added the `this.selectedBook.id` property to reuse the `CreateUpdateBook` action. - #### Add "Actions" Dropdown to the Table Open the `book-list.component.html` and add modify the `p-table` as shown below: @@ -471,15 +459,16 @@ Open the `book-list.component.html` and add modify the `p-table` as shown belo ``` -Actions button added to each row of the table. +* Added a `th` for the "Actions" column. +* Added `button` with `ngbDropdownToggle` to open actions when clicked the button. > We've used to [NgbDropdown](https://ng-bootstrap.github.io/#/components/dropdown/examples) for the dropdown menu of actions. -The Actions buttons looks like this: +The final UI looks like: ![actions-buttons](images/bookstore-actions-buttons.png) -Update the modal header for reuse the same modal. +Update the modal header to change the title based on the current operation: ```html @@ -489,26 +478,24 @@ Update the modal header for reuse the same modal. ![actions-buttons](images/bookstore-edit-modal.png) -TODO: description & screenshot ?? +### Deleting an Existing Book -### Delete a Book +#### BooksService -Open `books.service.ts` and the the `delete` method for delete a book with the `id` by performing an HTTP request to the related endpoint. +Open `books.service.ts` and add a `delete` method to delete a book with the `id` by performing an HTTP request to the related endpoint: ```typescript -delete(id: string): Observable { - const request: Rest.Request = { +delete(id: string): Observable { + return this.rest.request({ method: 'DELETE', - url: `/api/app/book/${id}`, - }; - - return this.rest.request(request); + url: `/api/app/book/${id}` + }); } ``` -### State Definitions +#### DeleteBook Action -Add an action named `BooksDelete` to `books.actions.ts` +Add an action named `DeleteBook` to `books.actions.ts`: ```typescript export class DeleteBook { @@ -517,7 +504,7 @@ export class DeleteBook { } ``` -Then, open the `books.state.ts` and add the `delete` method that will listen to a `CreateUpdateBook` action to create a book +Open the `books.state.ts` and add the `delete` method that will listen to the `DeleteBook` action to delete a book: ```typescript import { ... , DeleteBook } from '../actions/books.actions'; @@ -528,11 +515,12 @@ delete({ dispatch }: StateContext, { id }: DeleteBook) { } ``` -TODO: description?? +* Added `DeleteBook` to the import list. +* Uses `bookService` to delete the book. -### Add a Delete Button +#### Add a Delete Button -Open `book-list.component.html` and modify the `ngbDropdownMenu` for add the delete button as shown below: +Open `book-list.component.html` and modify the `ngbDropdownMenu` to add the delete button as shown below: ```html
@@ -543,13 +531,13 @@ Open `book-list.component.html` and modify the `ngbDropdownMenu` for add the del
``` -The final actions dropdown UI looks like this: +The final actions dropdown UI looks like below: ![bookstore-final-actions-dropdown](images/bookstore-final-actions-dropdown.png) -### Open Confirmation Popup +#### Delete Confirmation Dialog -Open `book-list.component.ts` and inject the `ConfirmationService` for show confirmation popup. +Open `book-list.component.ts` and inject the `ConfirmationService`. ```typescript import { ConfirmationService } from '@abp/ng.theme.shared'; @@ -560,7 +548,9 @@ constructor( ) ``` -Add the following method to `BookListComponent`. +> `ConfirmationService` is a simple service provided by ABP framework that internally uses the PrimeNG. + +Add a delete method to the `BookListComponent`: ```typescript import { ... , DeleteBook } from '../../store/actions'; @@ -577,10 +567,10 @@ delete(id: string, name: string) { } ``` -The `delete` method shows confirmation popup and listens to them. When close the popup, the subscribe block runs. If confirmed this popup, it will dispatch the `DeleteBook` action. - -The confirmation popup looks like this: +The `delete` method shows a confirmation popup and subscribes for the user response. `DeleteBook` action dispatched only if user clicks to the `Yes` button. The confirmation popup looks like below: ![bookstore-confirmation-popup](images/bookstore-confirmation-popup.png) -TODO: End +### Next Part + +See the [next part](Part-III.md) of this tutorial. \ No newline at end of file diff --git a/docs/en/Tutorials/Angular/Part-III.md b/docs/en/Tutorials/Angular/Part-III.md new file mode 100644 index 0000000000..ffc79de5b1 --- /dev/null +++ b/docs/en/Tutorials/Angular/Part-III.md @@ -0,0 +1,178 @@ +## ASP.NET Core MVC Tutorial - Part III + +### About this Tutorial + +This is the third part of the Angular tutorial series. See all parts: + +- [Part I: Create the project and a book list page](Part-I.md) +- [Part II: Create, Update and Delete books](Part-II.md) +- **Part III: Integration Tests (this tutorial)** + +You can access to the **source code** of the application from the [GitHub repository](https://github.com/abpframework/abp/tree/dev/samples/BookStore-Angular-MongoDb). + +### Test Projects in the Solution + +There are multiple test projects in the solution: + +![bookstore-test-projects-v2](images/bookstore-test-projects-v2.png) + +Each project is used to test the related application project. Test projects use 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 `BookStoreTestDataSeedContributor` class in the `Acme.BookStore.TestBase` project that creates some data to run tests on. + +Change the `BookStoreTestDataSeedContributor` class as show below: + +````C# +using System; +using System.Threading.Tasks; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Guids; + +namespace Acme.BookStore +{ + public class BookStoreTestDataSeedContributor + : IDataSeedContributor, ITransientDependency + { + private readonly IRepository _bookRepository; + private readonly IGuidGenerator _guidGenerator; + + public BookStoreTestDataSeedContributor( + IRepository bookRepository, + IGuidGenerator guidGenerator) + { + _bookRepository = bookRepository; + _guidGenerator = guidGenerator; + } + + public async Task SeedAsync(DataSeedContext context) + { + await _bookRepository.InsertAsync( + new Book + { + Id = _guidGenerator.Create(), + Name = "Test book 1", + Type = BookType.Fantastic, + PublishDate = new DateTime(2015, 05, 24), + Price = 21 + } + ); + + await _bookRepository.InsertAsync( + new Book + { + Id = _guidGenerator.Create(), + Name = "Test book 2", + Type = BookType.Science, + PublishDate = new DateTime(2014, 02, 11), + Price = 15 + } + ); + } + } +} +```` + +* Injected `IRepository` and used it in the `SeedAsync` to create two book entities as the test data. +* Used `IGuidGenerator` service to create GUIDs. While `Guid.NewGuid()` would perfectly work for testing, `IGuidGenerator` has additional features especially important while using real databases (see the [Guid generation document](../../Guid-Generation.md) for more). + +### 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 empty, ABP throws an `AbpValidationException`. + +Open the **Test Explorer Window** (use Test -> Windows -> Test Explorer menu if it is not visible) and **Run All** tests: + +![bookstore-appservice-tests](images/bookstore-appservice-tests.png) + +Congratulations, green icons show that tests have been successfully passed! \ No newline at end of file diff --git a/docs/en/Tutorials/Angular/images/bookstore-test-projects-v2.png b/docs/en/Tutorials/Angular/images/bookstore-test-projects-v2.png new file mode 100644 index 0000000000000000000000000000000000000000..8701164d7514d15b27b6f83dfde1536bc38f5c58 GIT binary patch literal 6864 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