Implementation and Testing
Prioritizing feature implementation
- All applications have dependencies between
protocols/use-cases/features in terms of what needs what before it can run
- If there is no game board you can't think about moving pieces on the board yet
- If there is no database persistence layer it would be a waste of time to work on saving the game state.
- From the feature dependency graph there are multiple successful paths to building your final app over time: the onion of how it will appear layer by layer over time
- Your job as a software developer is to find the most natural route through the dependency maze to your final app.
This is a difficult planning problem
- Since there are multiple people on your team, you need a "parallel programming plan" so multiple parts can proceed simultaneously with minimal blocking/conflicts.
- You never want to write un-testable code, bugs will fester there since the debugging will happen long after it was written and forgotten about
-- SO, you can only add on new bits of code that are testable.
- Don't forget prototyping: Make a Potemkim village of surface functionality
- Prototyping is particularly good for UIs
- Big libraries/frameworks you are using should be exercised first by a small artificial example
- Develop underlying communication protocols so they are independently testable components
Fixing an onion building processFor the course projects we take the following simple approach
- Define a subset of features to start with, the key features, which when implemented will give bare-bones functionality
- Reify the onion into a series of deadlines, regular (two-week for us) iterations with concrete goals of features/etc implemented in each successive layer/iteration
Design vs implement
Question: keep refining the design, or start coding?
Here are some rough guides on whether the design is far enough along:
- The domain model is clear (the game rules, etc)
- There is a proposed software architecture (languages, libraries, communication protocols, etc)
- There are GUI sketches for all key features
- The GUI sketches have all the actions/buttons mapped on to use-cases
- Key use-cases have some initial mapping onto classes and methods
Note that prototyping can happen as the design is still being refined.
Iteration / Release Planning
Software iterations and releasesAn iteration of a project is a planned global step in the development of a piece of software.
- An iteration should not be too big: add some features, modify the design to do one aspect differently, etc.
- Iterations give you many little deadlines to sucessfully hit
- Plan iterations to give the goal: before starting, make a proposal of what will be accomplished in the forthcoming iteration.
A release is a stable iteration released to some users.
Terminology (there is no common standard for naming releases, just common patterns):
- Alpha release, e.g. 3.1alpha: Some features of eventual version 3.1 haven't been implemented yet and there are quite a few bugs. More for in-house testing.
- Beta release: the feature set is frozen and all features more or less work, but there are still bugs. Outside advanced users can use it.
- Pre-release: a generic term for alpha or beta
- Unstable release: any release with less testing; similar to alpha
- Release candidate (RC): better than beta; on deck for shipment, infintesimal bugs only
- RTM (release to marketing) and GA (general availability): an "official" product release for customers
- Version number less than 1: an application that has yet to get beyond alpha/beta/pre in any release.
- Its important to have a plan of how your project's onion will unfold
- Proceeding without a plan is like driving a car where there is no road: its hard!
- A clear straight road is easy, a curved muddy road is hard to drive on.
- An interation plan maps features and/or use-cases on to which iteration they should be implemented in.
- Have a detailed iteration plan for the next iteration and a fuzzier one for more distant iterations
- Revise your iteration plan at the end of each iteration
- Maybe some things in the previous iteration proved too hard -- bump up to current iteration or divide into smaller problems over several future iterations.
- Make clear what the new set of features / use-cases you want to add in the next iteration is - take your fuzzy ideas from the previous iteration plan and refine them to a concrete plan.
- Keep iteration planning in mind when making use-cases
- the idea of a use-case is a manageable task for one iteration (at least the happy path through the use-case)
- if your use-cases are bigger than that they should probably be broken up.
Implementation PrinciplesHere are some random good principles for implementation, many of which are from the XP and Agile schools of thought.
Practice Collective Code Ownership
- Everyone can edit all code (use git or other version control system)
- Humans are social animals; sharing code will better share ideas/concepts/solutions
- allows refactoring to not be bounded--tweak others' code if you see a better way for it to interact with your code
- Aids in production of readable, elegant code (who wants to be hated by everyone else)
- But, it needs unit tests to work best -- if you tweak others' code, run their tests to make sure you didn't hose it.
Continuous IntegrationBuild the whole system on a regular basis, don't work in a sandbox for long
- Will make it more obvious when a recent change broke the system
- Works well with unit tests because its automatically apparent that the build failed.
- Implicitly means you are fixing small bugs and not big bugs.
Have a coding standardThis is a standard intro programming topic you probably know well.
See the Information page Style section for guidelines for this course; here are a couple more:
- Follow the Java API formats for Java coding, both in terms of names of methods, etc, and in terms of usage of JavaDoc.
- If a method returns a boolean, make that clear in the method
- Use active voice (imperative): i.e.: deleteInvoice()
- Use active voice for comments as well: deletes the Invoice not the invoice is deleted
- Don't re-use a temporary variable
Pair programming is two people programming on one terminal.
- Pair consists of driver & partner
- driver has the keyboard
- ALL coding done in pairs
- Partner: corrects flaws in driver
- asks questions of driver about code
- has final say (they have the keyboard)
- can focus on code details since parter is on the concepts
Is it good? Bad? Ugly?
- One brain has attention to "concepts", the other to "details" -- specializing in a way that makes a "superbrain" better than two individual brains trying to hold both at once.
- You get the best qualities of two people - some are better at concepts/critiquing and some better at details/typing.
- Rapidly train new people (intense exposure)
- Leverage that "humans are social animals" aspect again
- Disadvantages: With only one person typing, it can slow things down.
- Conclusion: works well with some personalities on some projects some or all of the time. Give it a test-drive if you have not tried it before.
Refactoring is an independent future lecture topic. At the root its very simple: sometimes its better to stop adding features and instead redo existing code to be more elegant and more aligned to they way you (now) see you are going.
TestingTesting is a major component of commercial software development.
- All code is obviously tested before shipping, the question is how thoroughly/frequently/automatically it is tested.
- A test suite is a set of tests which can be automatically run and the code either passes or fails.
- If you don't take a methodological approach to automated testing it is very difficult to develop large pieces of software
Testing HierarchiesWe use the simple XP testing hierarchy
- Unit tests (low-level operations)
- Acceptance tests (at the level of use-cases)
Unit TestingWrite small tests for each nontrivial operation
- Test should be completely automatically executable.
- Each test returns either true (success) or false (fail) always.
- Re-run the complete unit test suite after any significant change, and then immediately debug to get test success to 100%
- By being rigorous about regular and thorough testing you
are stamping out the bugs before they get out of hand.
-- "a stitch in time saves nine" yet again.
Unit testing in Java with JUnit
- JUnit is a simple Java unit testing framework
- It is also built into Eclipse: just hit the button and wait for the green light
- You are required to implement unit tests for your projects
- Learning JUnit is not hard and is a self-study topic for those not familiar with it; see the Course CASE tools page for pointers.
- There is no need for tests that completely overlap in terms of bugs they would catch: quality over quantity.
- If there is some special case the code should work on, document this by writing a test for it.
- If you think an operation could fail, write a test to make
sure you are catching it
(e.g. is reading past the end of file caught?)
- Add tests before refactoring to make sure you can verify the success of the refactoring afterwards
- Cover bugs with tests (i.e. add a test that would have failed given the bug you just found; prevents recurring bugs)
Degrees of importance of thorough testingThe degree of commitment varies by the size, complexity, and need for correctness of the project. Here are some points on the spectrum.
- Safety-critical systems require very thorough
testing since failure could be catastrophic
-- sometimes want to go beyond testing to formal verification of critical systems
- All significant software projects need some degree of automated testing to keep bugs down.
- Short temporary scripts have fairly obvious functionality and automated testing is not worth the overhead.
- Acceptance tests are tests corresponding to use-cases: one per use-case.
- Acceptance tests often involve clicking on GUI elements so special tools may be needed to automate acceptance testing.
Things that are harder to test automaticallyYou need to work harder to get some features automatically tested.
- GUI's: its hard to automate the input. Either test via the underlying model, or display in a console a message for what the human tester needs to manually do at each point.
- Distributed systems, persistence layers: make sure you set up a particular initial database/user configuration before running the tests.
- Chicken-egg problems: if you have no code, you have no concrete objects to pass as parameters. Its OK to just prototype it, pass mock objects initially.