This site is from a past semester! The current version will be here when the new semester starts.
TIC4001 2020
  • Full Timeline
  • Week 1 [Mon, Aug 10th]
  • Week 2 [Fri, Aug 14th]
  • Week 3 [Fri, Aug 21st]
  • Week 4 [Fri, Aug 28th]
  • Week 5 [Fri, Sep 4th]
  • Week 6 [Fri, Sep 11th]
  • Week 7 [Fri, Sep 18th]
  • Week 8 [Fri, Oct 2nd]
  • Week 9 [Fri, Oct 9th]
  • Week 10 [Fri, Oct 16th]
  • Week 11 [Fri, Oct 23rd]
  • Week 12 [Fri, Oct 30th]
  • Week 13 [Fri, Nov 6th]
  • Textbook
  • Admin Info
  • Report Bugs
  • Forum
  • Gitter (Chat)
  • Instructors
  • Announcements
  • Files
  • Java Coding Standard
  • Git Conventions
  • Participation Dashboard

  •  Individual Project (iP):
  • Individual Project Info
  • iP List
  • iP Upstream Repo
  • iP Code Dashboard
  • iP Progress Dashboard

  •  Team Project (tP):
  • Team Project Info
  • Team List
  • tP Code Dashboard
  • tP Progress Dashboard
  • Week 6 [Fri, Sep 11th] - Topics

    • [W6.1] IDEs: Intermediate Features
    • [W6.1a] Implementation → IDEs → Debugging → What :

    • [W6.1b] Tools → IntelliJ IDEA → Debugging: Basic

    • [W6.1c] Tools → IntelliJ IDEA → Productivity shortcuts :

    • [W6.2] Code Quality: Unsafe Practices
    • [W6.2a] Implementation → Code Quality → Error-Prone Practices → Introduction

    • [W6.2b] Implementation → Code Quality → Error-Prone Practices → Basic → Use the default branch

    • [W6.2c] Implementation → Code Quality → Error-Prone Practices → Basic → Don't recycle variables or parameters

    • [W6.2d] Implementation → Code Quality → Error-Prone Practices → Basic → Avoid empty catch blocks

    • [W6.2e] Implementation → Code Quality → Error-Prone Practices → Basic → Delete dead code

    • [W6.2f] Implementation → Code Quality → Error-Prone Practices → Intermediate → Minimize scope of variables

    • [W6.2g] Implementation → Code Quality → Error-Prone Practices → Intermediate → Minimize code duplication

    • [W6.3] Developer Testing
    • [W6.3a] Quality Assurance → Testing → Developer Testing → What :

    • [W6.3b] Quality Assurance → Testing → Developer Testing → Why :

    • [W6.4] Unit Testing
    • [W6.4a] Quality Assurance → Testing → Test Automation → Test automation using test drivers :

    • [W6.4b] Quality Assurance → Testing → Test Automation → Test automation tools :

    • [W6.4c] Quality Assurance → Testing → Unit Testing → What :

    • [W6.4d] C++ to Java → JUnit → JUnit: Basic :


    [W6.1] IDEs: Intermediate Features

    W6.1a :

    Implementation → IDEs → Debugging → What

    Video

    Can explain debugging

    Debugging is the process of discovering defects in the program. Here are some approaches to debugging:

    • Bad -- By inserting temporary print statements: This is an ad-hoc approach in which print statements are inserted in the program to print information relevant to debugging, such as variable values. e.g. Exiting process() method, x is 5.347. This approach is not recommended due to these reasons:
      • Incurs extra effort when inserting and removing the print statements.
      • These extraneous program modifications increase the risk of introducing errors into the program.
      • These print statements, if not removed promptly after the debugging, may even appear unexpectedly in the production version.
    • Bad -- By manually tracing through the code: Otherwise known as ‘eye-balling’, this approach doesn't have the cons of the previous approach, but it too is not recommended (other than as a 'quick try') due to these reasons:
      • It is a difficult, time consuming, and error-prone technique.
      • If you didn't spot the error while writing the code, you might not spot the error when reading the code either.
    • Good -- Using a debugger: A debugger tool allows you to pause the execution, then step through the code one statement at a time while examining the internal state if necessary. Most IDEs come with an inbuilt debugger. This is the recommended approach for debugging.

    W6.1b

    Tools → IntelliJ IDEA → Debugging: Basic

    Can step through a program using a debugger

    This video (from LaunchCode) gives a pretty good explanation of how to use the IntelliJ IDEA debugger.

    W6.1c :

    Tools → IntelliJ IDEA → Productivity shortcuts

    Can use some useful IDE productivity shortcuts

    [W6.2] Code Quality: Unsafe Practices

    W6.2a

    Implementation → Code Quality → Error-Prone Practices → Introduction

    Can explain the need for avoiding error-prone shortcuts

    It is safer to use language constructs in the way they are meant to be used, even if the language allows shortcuts. Such coding practices are common sources of bugs. Know them and avoid them.

    W6.2b

    Implementation → Code Quality → Error-Prone Practices → Basic → Use the default branch

    Can improve code quality using technique: use the default branch

    Always include a default branch in case statements.

    Furthermore, use it for the intended default action and not just to execute the last option. If there is no default action, you can use the default branch to detect errors (i.e. if execution reached the default branch, raise a suitable error). This also applies to the final else of an if-else construct. That is, the final else should mean 'everything else', and not the final option. Do not use else when an if condition can be explicitly specified, unless there is absolutely no other possibility.

    Bad

    if (red) print "red";
    else print "blue";

    Good

    if (red) print "red";
    else if (blue) print "blue";
    else error("incorrect input");

    W6.2c

    Implementation → Code Quality → Error-Prone Practices → Basic → Don't recycle variables or parameters

    Can improve code quality using technique: don't recycle variables or parameters

    • Use one variable for one purpose. Do not reuse a variable for a different purpose other than its intended one, just because the data type is the same.
    • Do not reuse formal parameters as local variables inside the method.

    Bad

    double computeRectangleArea(double length, double width) {
    length = length * width;
    return length;
    }
    def compute_rectangle_area(length, width):
    length = length * width
    return length

    Good

    double computeRectangleArea(double length, double width) {
    double area;
    area = length * width;
    return area;
    }
    def compute_rectangle_area(length, width):
    area = length * width
    return area
    }

    W6.2d

    Implementation → Code Quality → Error-Prone Practices → Basic → Avoid empty catch blocks

    Can improve code quality using technique: avoid empty catch blocks

    Never write an empty catch statement. At least give a comment to explain why the catch block is left empty.

    W6.2e

    Implementation → Code Quality → Error-Prone Practices → Basic → Delete dead code

    Can improve code quality using technique: delete dead code

    You might feel reluctant to delete code you have painstakingly written, even if you have no use for that code anymore ("I spent a lot of time writing that code; what if I need it again?"). Consider all code as baggage you have to carry; get rid of unused code the moment it becomes redundant. If you need that code again, simply recover it from the revision control tool you are using. Deleting code you wrote previously is a sign that you are improving.

    W6.2f

    Implementation → Code Quality → Error-Prone Practices → Intermediate → Minimize scope of variables

    Can improve code quality using technique: minimize scope of variables

    Minimize global variables. Global variables may be the most convenient way to pass information around, but they do create implicit links between code segments that use the global variable. Avoid them as much as possible.

    Define variables in the least possible scope. For example, if the variable is used only within the if block of the conditional statement, it should be declared inside that if block.

    The most powerful technique for minimizing the scope of a local variable is to declare it where it is first used. -- Effective Java, by Joshua Bloch

    Resources:

    W6.2g

    Implementation → Code Quality → Error-Prone Practices → Intermediate → Minimize code duplication

    Can improve code quality using technique: minimize code duplication

    Code duplication, especially when you copy-paste-modify code, often indicates a poor quality implementation. While it may not be possible to have zero duplication, always think twice before duplicating code; most often there is a better alternative.

    This guideline is closely related to the DRY Principle.

    Principles →

    DRY principle

    DRY (Don't Repeat Yourself) principle: Every piece of knowledge must have a single, unambiguous, authoritative representation within a system. -- The Pragmatic Programmer, by Andy Hunt and Dave Thomas

    This principle guards against the duplication of information.

    A functionality being implemented twice is a violation of the DRY principle even if the two implementations are different.

    The value of a system-wide timeout being defined in multiple places is a violation of DRY.

    [W6.3] Developer Testing

    W6.3a :

    Quality Assurance → Testing → Developer Testing → What

    Can explain developer testing

    Developer testing is the testing done by the developers themselves as opposed to professional testers or end-users.

    W6.3b :

    Quality Assurance → Testing → Developer Testing → Why

    Video

    Can explain the need for early developer testing

    Delaying testing until the full product is complete has a number of disadvantages:

    • Locating the cause of a test case failure is difficult due to a large search space; in a large system, the search space could be millions of lines of code, written by hundreds of developers! The failure may also be due to multiple inter-related bugs.
    • Fixing a bug found during such testing could result in major rework, especially if the bug originated from the design or during requirements specification i.e. a faulty design or faulty requirements.
    • One bug might 'hide' other bugs, which could emerge only after the first bug is fixed.
    • The delivery may have to be delayed if too many bugs are found during testing.

    Therefore, it is better to do early testing, as hinted by the popular rule of thumb given below, also illustrated by the graph below it.

    The earlier a bug is found, the easier and cheaper to have it fixed.

    Such early testing of partially developed software is usually, and by necessity, done by the developers themselves i.e. developer testing.

    Discuss the pros and cons of developers testing their own code.

    Pros:

    • Can be done early (the earlier we find a bug, the cheaper it is to fix).
    • Can be done at lower levels, for example, at the operation and class levels (testers usually test the system at the UI level).
    • It is possible to do more thorough testing because developers know the expected external behavior as well as the internal structure of the component.
    • It forces developers to take responsibility for their own work (they cannot claim that "testing is the job of the testers").

    Cons:

    • A developer may subconsciously only test situations that he knows to work (i.e. test it too 'gently').
    • A developer may be blind to his own mistakes (if he did not consider a certain combination of input while writing the code, it is possible for him to miss it again during testing).
    • A developer may have misunderstood what the SUT is supposed to do in the first place.
    • A developer may lack the testing expertise.

    The cost of fixing a bug goes down as we reach the product release.

    False. The cost goes up over time.

    Explain why early testing by developers is important.

    [W6.4] Unit Testing

    Video

    W6.4a :

    Quality Assurance → Testing → Test Automation → Test automation using test drivers

    Can explain test drivers

    A test driver is the code that ‘drives’ the Software Under TestSUT for the purpose of testing i.e. invoking the SUT with test inputs and verifying if the behavior is as expected.

    PayrollTest ‘drives’ the Payroll class by sending it test inputs and verifies if the output is as expected.

    public class PayrollTest {
    public static void main(String[] args) throws Exception {

    // test setup
    Payroll p = new Payroll();

    // test case 1
    p.setEmployees(new String[]{"E001", "E002"});
    // automatically verify the response
    if (p.totalSalary() != 6400) {
    throw new Error("case 1 failed ");
    }

    // test case 2
    p.setEmployees(new String[]{"E001"});
    if (p.totalSalary() != 2300) {
    throw new Error("case 2 failed ");
    }

    // more tests...

    System.out.println("All tests passed");
    }
    }

    W6.4b :

    Quality Assurance → Testing → Test Automation → Test automation tools

    Can explain test automation tools

    JUnit is a tool for automated testing of Java programs. Similar tools are available for other languages and for automating different types of testing.

    This is an automated test for a Payroll class, written using JUnit libraries.

    @Test
    public void testTotalSalary() {
    Payroll p = new Payroll();

    // test case 1
    p.setEmployees(new String[]{"E001", "E002"});
    assertEquals(6400, p.totalSalary());

    // test case 2
    p.setEmployees(new String[]{"E001"});
    assertEquals(2300, p.totalSalary());

    // more tests...
    }

    Most modern IDEs have integrated support for testing tools. The figure below shows the JUnit output when running some JUnit tests using the Eclipse IDE.

    W6.4c :

    Quality Assurance → Testing → Unit Testing → What

    Can explain unit testing

    Unit testing: testing individual units (methods, classes, subsystems, ...) to ensure each piece works correctly.

    In OOP code, it is common to write one or more unit tests for each public method of a class.

    Here are the code skeletons for a Foo class containing two methods and a FooTest class that contains unit tests for those two methods.

    class Foo {
    String read() {
    // ...
    }

    void write(String input) {
    // ...
    }

    }
    class FooTest {

    @Test
    void read() {
    // a unit test for Foo#read() method
    }

    @Test
    void write_emptyInput_exceptionThrown() {
    // a unit tests for Foo#write(String) method
    }

    @Test
    void write_normalInput_writtenCorrectly() {
    // another unit tests for Foo#write(String) method
    }
    }
    import unittest

    class Foo:
    def read(self):
    # ...

    def write(self, input):
    # ...


    class FooTest(unittest.TestCase):

    def test_read(self):
    # a unit test for read() method

    def test_write_emptyIntput_ignored(self):
    # a unit test for write(string) method

    def test_write_normalInput_writtenCorrectly(self):
    # another unit test for write(string) method

    Side readings:

    W6.4d :

    C++ to Java → JUnit → JUnit: Basic

    Can use simple JUnit tests

    When writing JUnit tests for a class Foo, the common practice is to create a FooTest class, which will contain various test methods.

    Suppose we want to write tests for the IntPair class below.

    public class IntPair {
    int first;
    int second;

    public IntPair(int first, int second) {
    this.first = first;
    this.second = second;
    }

    public int intDivision() throws Exception {
    if (second == 0){
    throw new Exception("Divisor is zero");
    }
    return first/second;
    }

    @Override
    public String toString() {
    return first + "," + second;
    }
    }

    Here's a IntPairTest class to match (using JUnit 5).

    import org.junit.jupiter.api.Test;

    import static org.junit.jupiter.api.Assertions.assertEquals;
    import static org.junit.jupiter.api.Assertions.fail;

    public class IntPairTest {


    @Test
    public void testStringConversion() {
    assertEquals("4,7", new IntPair(4, 7).toString());
    }

    @Test
    public void intDivision_nonZeroDivisor_success() throws Exception {
    assertEquals(2, new IntPair(4, 2).intDivision());
    assertEquals(0, new IntPair(1, 2).intDivision());
    assertEquals(0, new IntPair(0, 5).intDivision());
    }

    @Test
    public void intDivision_zeroDivisor_exceptionThrown() {
    try {
    assertEquals(0, new IntPair(1, 0).intDivision());
    fail(); // the test should not reach this line
    } catch (Exception e) {
    assertEquals("Divisor is zero", e.getMessage());
    }
    }
    }

    Notes:

    • Each test method is marked with a @Test annotation.
    • Tests use Assert.assertEquals(expected, actual) methods to compare the expected output with the actual output. If they do not match, the test will fail. JUnit comes with other similar methods such as Assert.assertNull and Assert.assertTrue.
    • Java code normally use camelCase for method names e.g., testStringConversion but when writing test methods, sometimes another convention is used: whatIsBeingTested_descriptionOfTestInputs_expectedOutcome e.g., intDivision_zeroDivisor_exceptionThrown
    • There are several ways to verify the code throws the correct exception. The third test method in the example above shows one of the simpler methods. If the exception is thrown, it will be caught and further verified inside the catch block. But if it is not thrown as expected, the test will reach Assert.fail() line and will fail as a result.
    • The easiest way to run JUnit tests is to do it via the IDE. For example, in Intellij you can right-click the folder containing test classes and choose 'Run all tests...'

    Adding JUnit 5 to your IntelliJ Project -- by Kevintroko@YouTube