CMSI 543 / SYEG 557: Welcome to Week 10

This Week's Class Agenda

What's Going On This Week

  1. Announcements
  2. Quality
  3. Refactored Code
  4. Test Driven Development [TDD]
  5. Example using TDD
  6. In-class TDD exercise

Quality

One of the Principles of the Agile Manifesto is the idea of Continuous attention to technical excellence and good design. As quoted in your textbook on page 180:

Testing does enhance quality, but it is really more of a discipline for helping developers work safely on code in brain-sized chunks. It's more about problem decomposition and managing cognitive load than about quality. [emphasis mine]

On the other hand, the tests abide after the programming is done, and all of those unit tests [microtests] we write while doing TDD are there to help us know if the code we are currently working on breaks any existing code. [emphasis mine]

[Tim Ottinger, 2013, in Ashmore, p. 180]

Creating a Quality-Focused Culture

What we need is an organizational culture that is focused on quality. This requires much more than simply writing a plan and tracking to it, which is the normal way of doing things under Waterfall. Agile takes a much more proactive approach to quality than Waterfall [which is 'reactive']. Under Waterfall, each part of the application may have been tested at the lower level to show it works, but there may be no sense of how well that code will work when it's integrated to make the full product. Agile, by constrast, normally uses daily builds that integrate all of the currently working code into the application and runs tests on the entire build. That way, if the build breaks, the team gets immediate feedback about what is broken [and often where it is broken], so that the situation can be addressed and resolved right away. This is advantageous because it is likely that the code is still fresh in the developer's mind. Further, there will be a lot less code that has to be interpreted or waded through to locate the defect.

Pair Programming

We've actually seen this before, but it's worth re-visiting. Introduced by the XP method, it has two people shoulder-to-shoulder at the same computer working together, and trading off every 15-20 minutes. One is the typist, called the driver, and the other is the observer [in name only].

There are several benefits to this approach in terms of quality:

  • Both programmers learn from each other
  • Both become more knowledgeable about the product they are creating
  • Each developer can focus on their specific role, so there is less cognitive load
  • Trust is built through the exchange, which fosters more regular communication
It is called collaboration

Test-Driven Development [Unit Testing]

Also commonly called by its acronym TDD, Test-Driven Development started with XP and has really become kind of a de-facto standard for the way developers build and test their code. In Waterfall, the usual way was to develop the code, then write the tests and run them later, near the end of the project life-cycle. With Agile, however, the situation is pretty much reversed; the developer writes the tests first, then does the code for the test, making it work properly with that test before moving on.

The idea includes writing ONE TEST AT A TIME, and only focusing on the code THAT WILL MAKE THAT TEST PASS. Only one test is focused on and the developer will not move on until that test passes. One of the good things this process fosters is that the developers must think about the requirements of the thing they are working on, so that they can effectively test it in the way it will be used. That way they can think not only about the requirements, but also about the context in which that method or function or class will be in when it is part of the entire product.

Doesn't it take extra time to do this? Isn't it less efficient to have two people working at the same time on the same computer? Actually, the answer is NO to both. It may take a bit more time to write the test first for EVERY module or method or function, but it actually saves time over the course of the project because the test suite is more complete and is being built up as the product is being developed so that it is ready and complete [or nearly so] by the time the application is complete. In response to the second question, it is actually MORE efficient, because with both people focused on the same code, you get better error checking and defect identification than with one person alone, so you actually end up with shorter development time.

So, how does one go about this? There are five basic steps:

Refactored Code

Gets rid of those harsh code smells

Refactoring is defined as the process of changing a software system in such a way that it does not alter the external behavior of the code yet improves its internal structure. What this means is that as long as you don't do anything to the way the code works, you can change the internal make up of the code however you want to make it cleaner.

Be careful not to confuse this activity with debugging the code. Refactoring is NOT debugging. The code has already been debugged, you are just improving it in a way that is benign to the operation of the overall product.

One of the nice things about TDD is that since you have a complete set of tests for everything, you can test each refactoring change and MAKE SURE you haven't broken anything. You can immediately find out if your change has caused damage, and can return to the previous version and find another way. Most of the time with refactoring this will not occur, and it will not be a problem.


The idea of refactoring originally came from mathematics, since you are reducing the code in the same way you might reduce an equation, until no additional changes can be made.

It is possible, however, that sometimes refactoring can introduce bugs. Because of the way it is defined, you may make changes to the internal structure of the code, as long as the way the code fits and operates in the outer context is unchanged. This might mean implimenting a completely different algorithm internally for a method, with the stipulation that the outer calling signature, the allowed inputs, and the expected outputs don't change.

For example, consider a method that implements a way of calculating the greatest common factor for two numbers. There are several well-known ways of doing this, but perhaps you started with a brute force algorithm. You test the algorithm using several tests, including edge cases, and everything is working fine. You then realize that it might be more effective to use Euclid's algorithm and you decide to clean up the code by implementing that far superior solution. At first, you may get an error or two when you run the tests, but as you refine the code you will get rid of them so that the new way of calculating passes all the tests. You have thus refactored the code so that not only do the same tests pass, but the same usage in the rest of the application will produce the same results as the way it was implimented previously.

Another consideration that refactoring brings to light is the idea of coupling. This is the extent to which there is interdependence between modules of code. For example, if one module is greatly dependent on one or more other modules, we say it is tightly coupled. If there is a mild dependence we say it is loosely coupled. While coupling is the term for BETWEEN modules, the corresponding term used for WITHIN modules is cohesion. This term means the degree to which the elements INSIDE a module are tied together. Most of the time in our code, we want the modulse to be LOOSELY coupled, and exhibit TIGHT cohesion.

Loosely Coupled, but Tight Cohesion

Example using TDD

Your textbook has a very complete example of Test Driven Development and Refactoring that begins on page 183 and continues through page 197. In these 15 pages the text walks you through creating and testing a game of BlackJack for a casino customer. The rules of the game are given first, so that those who are not familiar with the game can understand what is supposed to happen. Then the initial setup of the environment is discussed in a set of steps that puts the development team in a place that is ready to write code. This process is based on the use of JUnit, which is a much-used testing framework for the Java programming language.

In the subsequent pages, the text walks through nine different scenarios, giving highlights of the process at each step:

  1. Test Scenario 1: write a test to verify dealing the first two cards. The test is called testDealFirstTwoCardsToPlayerSecondTwoCardsToDealer because that is what the test will verify. Notice that this is a VERY descriptive name and in spite of its length will let the reader of the code know EXACTLY what is being tested. This is actually a good coding practice, not only for Java, but for other languages as well. Next, several method calls are set up, to test the functionality of the class that will be tested. The tests should be run so that the developers can see them fail. Finally the code that will be tested is written, calling the dealCard() method twice to deal the first two cards to the player, then twice more for the dealer.
  2. Test Scenario 2 – 4: in the next three cases, we test the situations in which both the dealer and the player have two cards, and verify that the correct operation happens with the method playHand(). All three possible scenarios for the cases are tested.
  3. Test Scenario 5: in this one, the dealer wins because the player keeps asking for one more card until her score exceeds 21. This scenario verifies the dealing of one card at a time to the player, as well as checking that the player's score has gone over 21.
  4. Test Scenario 6: this case tests the opposite, the player wins because the dealer had gone bust.
  5. Test Scenario 7: in this case, the player wins because she has a higher value hand than the dealer. This verifies the function to deal and verify that the scores are managed properly, as well as the functionality to stop at a certain point and stand.
  6. Test Scenario 8: again, the opposite case of the previous one, the dealer wins with a higher score once both players have decided to stand.
  7. Test Scenario 9: at last we have verification that there can be a tie with both the dealer and the player having identical scores.

Minimizing the Defect Backlog

In the best case, there would be NO defects at all, but of course, that is not very realistic unless the code is relatively simple and is crafted with extreme care and attention to detail. Barring that, there WILL be defects that must be dealt with, even in the most thoughtfully developed code.

It is important to fix defects right away, even to the point of prioritizing them higher than writing new code. This makes sense, since writing new code that is based on buggy code only makes the problem worse as the team applies "band-aids" to new code to make it work with older code, possibly even having to modify the product design in the process. Further, it is usually easier to find and fix problems while the code is fresh in the developers' minds rather than waiting, so it's more efficient to do it right away. Finally, in a team environment, waiting makes it even more likely that a different person on the team has modified that code and is unaware of the problems that are being built upon; this situation increases the likelihood of a defect making it into released software, with the associated loss of reputation which can cost in customer satisfaction.

For these reasons, it is important that proper testing be done at ALL levels, and that any defects which may be uncovered are dealt with immediately, or certainly within the next sprint.

Do not know where this came from, I have had it so long...

Usually, most defects are found very early, during the coding process itself. It is natural for developers to test their code, and even more likely if they are using TDD. However, these are developmental bugs that are fairly isolated to the particular modules. To help deal with this situation, Agile focuses on what is called continual integration builds in which the code is tested as soon as it is committed to the repository as part of the overall build testing. THIS DOES NOT MEAN THAT UPDATES ARE PART OF LIVE PRODUCTS. That would be stupid. However, it does mean that an integrated build is available to the team for testing at all times,

Customer feedback should also be part of some [not all] of the testing process. Alpha tests are used to allow the customers an early view, so they can try things out. This activity is often followed by Beta tests which incorporate feedback from Alpha and let the customers perform even more thorough testing, often using the product in its intended environment. Finally, there is Usability testing which involves the team watching the customer use the product to see where there may be opportunities to improve interaction with it.

Types of Testing

There are many different types of testing but they fall into two main categories.

In-class TDD exercise

For this in-class exercise, I'm going to walk you through some testing philosophies using the Java language. I'll be doing what's called live coding for which you are free to follow along as I code. To do this you will need an editor and the Java JDK installed on your computer; many of you may already have this this set up on your computers. If not, you can download the java installer from this location for both mac and PC. If you want to take some time to do that, it's fine; it only takes a few minutes and you can probably get it going over the mid-class break.

The link goes to the Oracle site for Java 15. This demonstration WILL NOT be using the JUnit 5 testing framework simply because so much has been added to it that it would take an entire class to get it working properly for everyone here. There is STILL a lot to JUnit 4, but it is easier to get working so we'll use that. Here are some links that you can use to get things set up and running:

The itemized activities I'll demonstrate are as follows:

  1. Edit, compile, and run a trivial Java Class file to verify the development environment.
  2. Edit, compile, and run a second file that will be the test harness.
  3. Add an empty method into the class from step 1.
  4. Add a test to the test harness to exercise the empty method.
  5. Run the test harness to see it fail since the method being tested is empty.
  6. Fill out the method in the class file.
  7. Re-run the test harness and see the tests pass. If it fails, figure out why and fix it, then run the test over. Keep doing this until the test passes.
  8. Add another empty method into the class.
  9. Add several methods into the test harness to check inputs and expected outputs for the new method, then run them to see them fail since the methods being tested are empty.
  10. Fill out the new methods in the class file, one at a time.
  11. Re-run the test harness and see the tests pass/fail. If something fails when you think it should pass, figure out why and fix it, then do the tests over. Keep doing this until the tests ALL pass, including the ones from the previous method and test runs.
  12. Re-factor the code for the .java class file and the test harness to clean it up.
  13. Re-run the tests harness and see the tests pass. If something fails, figure out why and fix it, then do the tests over. Keep doing this until the tests ALL pass, including the ones from the previous method and test runs.
  14. Commit your code to your GitHub repo under the folder "week10".
  15. Pat yourself on the back for a job well done! Feel free to pat your groupmates on the back, too!