Lab 6 - Automatic Unit Tests, Refactoring & Functional Test Planning
Introduction
In this Lab, you will cover the POS system with unit tests, complete some refactoring tasks, and create a test plan. Recall from Lab 4 that you were suggested to write the test cases during the development phase, so at this point nothing is new regarding the automatic tests of Task 1. If you have already done the tests, you will only have to show them; otherwise, this is your last chance to add the unit tests to the project and show them.
Homework
Task 1. Covering POS with Automatic Unit Tests. (4.5p)
Add JUnit support by adding the following line to build.gradle
testCompile group: 'junit', name: 'junit', version: '4.+'
Created tests should be stored in 'src\test\java' in the corresponding package.
Most of the applications (and SalesSystem is not an exception) have many classes. Some of them are difficult or not reasonable to cover with unit tests (they depend on many external actions, etc). Ease of the unit test development is a sign of a good architecture and design.
Unit tests should not access the real database. This is important so that the test remain fast. To avoid interacting with the database you can mock out the database. This means that you make the application think that it is interacting with a database but in reality it is not.
With the current application design this should be easy to do. The only class interacting with the database directly is the HibernateSalesSystemDAO. Before adding database support the program used InMemorySalesSystemDAO which looked like access to the database, but in reality had hard coded data and kept changes in the memory.
You should create a new SaleSystemDAO implementation that you can use in unit tests or modify InMemorySalesSystemDAO so it can also be used for tests.
All tests should follow the following rules:
- Tests should not use the database, the data-access-object HibernateSalesSystemDAO should not be used
- Classes SalesSystemGUI and SalesSystemCLI cannot be used for running tests
- Use JUnit tests, do not run tests from a main method, let JUnit take care of running tests
As a guidance, we suggest that you write the following tests. If your test cases differ in names you should be able to explain why the same functionality is still covered:
Warehouse management
- addItem
- testAddingItemBeginsAndCommitsTransaction - check that methods beginTransaction and commitTransaction are both called exactly once and that order
- testAddingNewItem - check that a new item is saved through the DAO
- testAddingExistingItem - check that adding a new item increases the quantity and the saveStockItem method of the DAO is not called
- testAddingItemWithNegativeQuantity - check that adding an item with negative quantity results in an exception
Shopping cart
- addItem
- testAddingExistingItem - check that adding an existing item increases the quantity
- testAddingNewItem - check that the new item is added to the shopping cart
- testAddingItemWithNegativeQuantity - check that an exception is thrown if trying to add item with negative quantity
- testAddingItemWithQuantityTooLarge - check that an exception is thrown if the quantity of the added item is larger than quantity in warehouse
- testAddingItemWithQuantitySumTooLarge - check that an exception is thrown if the sum of the quantity of the added item and the quantity already in shopping cart is larger than quantity in warehouse
- submitCurrentPurchase
- testSubmittingCurrentPurchaseDecreasesStockItemQuantity - check that submitting the current purchase decreases the quantity of all StockItems
- testSubmittingCurrentPurchaseBeginsAndCommitsTransaction - check that submitting the current purchase calls beginTransaction and endTransaction, exactly once and in that order
- testSubmittingCurrentOrderCreatesHistoryItem - check that a new HistoryItem is saved and that it contains the correct SoldItems
- testSubmittingCurrentOrderSavesCorrectTime - check that the timestamp on the created HistoryItem is set correctly (for example has only a small difference to the current time)
- testCancellingOrder - check that cancelling an order (with some items) and then submitting a new order (with some different items) only saves the items from the new order (cancelled items are discarded)
- testCancellingOrderQuanititesUnchanged - check that after cancelling an order the quantities of the related StockItems are not changed
Task 2. Refactoring. (2.5p)
Clean code should follow the basic principles listed below:
- DAO classes should not contain any business logic. DAO classes should only save and retrieve objects. This also makes writing tests easier.
- GUI and CLI should not contain any business logic. Everything that is done in both GUI and CLI projects, has to be written in the common project. Code should not be repeated (create a reusable methods instead of copy-pasting code).
- Every class follows the single responsibility principle (SRP): a class handles only one thing and does not have to know how other classes solve problems. Look at the public methods of each of your classes and try to describe the purpose of that class. If the description contains "and", then probably the class is violating SRP.
- Names of classes, methods and variable have to correspond to their content. Code comments don't substitute good variable/field/method names. Don't be afraid of long and descriptive names - they are much more useful than short and obscure names. If naming a method is difficult, then maybe the method is doing too many different things. Apply SRP and break it into smaller independant methods which are easier to name.
- GUI has to be defined in FXML. GUI classes should only include event handling.
You should go through your code and check if all classes correspond to the 5 basic principles. If any principle is violated you have to fix your code.
The purpose of refactoring is readability and testability. Write clean and readable code - not for yourself but for your teammates. Code is read many more times than it is written. Having others decipher your sloppily written spaghetti wastes everyone's time. Write good tests not to prove that your code is correct, but to protect your masterpiece from your teammates. Otherwise they can introduce bugs into it when making changes.
Task 3. Functional test plan. (3p)
Create a test plan for the verification of the POS application functional requirements (produced in the first lab). You can use a Google spreadsheet (optional) extending the project plan. The functional test plan should have the following:
- Functionality to be verified
- Assignee (for each requirement)
- Usage frequency of the functionality to be verified: low/average/high (for each requirement)
- Possible damage for business on failure: low/average/high (for each requirement)
- Priority: low/average/high (for each requirement)
- Estimated test effort in person-hours (for each requirement)
Functionality to be verified is just a functional requirement (User Story). Assignee is one of your team members. Usage frequency is your estimate to the functionality usage rate. If it is used seldom, like a report in the end of the week, then it would have "low" usage frequency. If it is used frequently, like making an order, then it will have a "high" usage frequency.
The possible damage for the business on failure should show how critical this functionality is for the business process. If you cannot accept the payments then it obviously should be "high".
The priority is based on the usage frequency and possible damage for business estimates. If both are high then priority is high. If both are low then priority is also low. But if they are different you need to make decision yourself based on the requirement under consideration.
Estimated effort can be either numeric in person-hours or also defined by a class like low/medium/high. If you will use classes, then you need to define the numeric range in person-hours for each of the classes. The estimated test effort includes test preparation (designing of the test cases), execution of the test case and reporting of the execution results. The estimated effort does not include debugging and problem correction.
The functional test plan would clearly state who, when, what and how to test. We cover most of these elements in different tasks.
The test plan must also contain specified test cases. Create at least 5 test cases for your functionality testing. These test cases should cover at least 5 non-trivial functional requirements. It does make sense to use the functional requirements covered with the use cases. Your test cases should have at least the following elements:
- Test case ID
- Related requirement(s)
- Input that is used for the test to verify the functionality. Pay special attention to the borderline cases and exceptions.
- Steps performed during the verification process
- Expected results
You can change/improve your requirements to make them more suitable for this task.
Submission of Results
Your team must submit the results of the following tasks:
- A set of unit tests covering the main functionality of the POS System. We suggest you create at least the list of tests as specified in task 1. (4.5p)
- Code refactoring: Your code on Bitbucket must follow the basic 5 principles of clean code described in task 2. (2.5p)
- A functional test plan: The plan must include all the requirements already defined for the basic functionality of the POS system. As stated in task 3, in addition, you must specify 5 functional test cases. (3p)
Your results must be submitted using your Bitbucket repository and the Bitbucket wiki. Create a new tag named "homework-6" and add a link in the Bitbucket wiki page.
NOTE: In the assessment/consultation lab you must be able to show partial solutions of all tasks.
Links
- JUnit
- Code Smells
- List of code smells (wikipedia): https://en.wikipedia.org/wiki/Code_smell
- List of code smells (by Jeff Atwood): http://blog.codinghorror.com/code-smells/
- Catalogue of refactorings (by Martin Fowler): http://refactoring.com/catalog/
- List of smells with suggested refactorings: (link)