11. Deployment and Testing

Deployment Models

A deployment diagram/model is an undirected graph showing nodes, components, and connections.

A node represents a physical computational resource. UML uses cube-shaped icons to represent nodes.

There are two node stereotypes: <<processor>> and <<device>>. A processor has processing and storage capabilities, such as a computer or a database manager. A device may have storage capabilities, but not processing capabilities. Printers, terminals, and disk drives are examples of devices.

Nodes may have attributes such as name, capacity, speed, and reliability. The name of a node may include its type:

big-blue: MainFrame

An association between two nodes represents a physical connection. Connections can be stereotyped by communication protocols:

<<TCP/IP>>, <<UDP/IP>>, <<X.25>>, <<Frame Relay>>, <<ATM>>
<<HTTP>>, <<FTP>>, <<Telnet>>, <<Email>>
<<RS-232>>, <<modem>>
<<PSTN>>, <<ISDN>>
<<Ethernet>>, <<Token Ring>>, <<Token Bus>>
<<ODBC>>, <<RMI>>, <<RPC>>, <<COM>>, <<CORBA>>

Components

Objects and components may reside on or are assigned to a node.

In UML, a component is, "a physical, replaceable, part of a system that realizes one or more interfaces." A component icon is a rectangle with tabs. An interface is a labeled circle.

This definition is pretty good. I would simplify it further by simply defining a component to be any object that is known to its clients through one or more interfaces that it implements. "Replaceable" and "physical" more or less follow as consequences. When most people think of components, they think of Java Beans and/or COM objects such as Active X controls. Both of these are covered under both definitions. Of course one might well ask why we can't simply use object diagrams to represent components.

Another issue is what is meant by the term "interface". The Java definition of interface as a collection of function signatures is clearly not complete enough to allow us to develop replaceable objects. In fact, the definition of interface used when talking about Java Beans is extended to include how properties are accessed and changed. COM interfaces include meta interfaces that allow clients to query a component about its properties, the events it fires, and the services it provides.

UML claims its definition of component should also include files (executable and data), libraries (dynamically and statically linked), documents, and tables. (These are all component stereotypes). I find this extension a bit confusing.

Examples

From Rose:

Done using the Word diagram editor:

Testing

Here's a Rose model that shows testing concepts.

There are (at least) three dimensions to testing:

In theory, we can devise tests that combine any three of these dimensions.
 
 

Level

Component Level

Test design and implementation of a function or class. Also called unit testing. Programmers should create a test harness as a static member function for every class they create:

Integration Level

Test design and implementation of collaborations and subsystems.

System Level

Internal test of the function of the entire system. (alpha testing)

Acceptance Level

Stakeholder test of the function of the entire system. (beta testing) All tests should be test cases associated with use cases specified at the beginning of the development process.

Structural vs. Functional

Structural analysis verifies the system's design. Functional analysis validates the system's behavior.

Dynamic vs. Static

Dynamic testing involves executing the program. Static testing involves inspecting the code. Code reviews (walkthroughs and inspections) and correctness proofs are well known examples of static testing.
 
 

Testing Plan

1. Decide who will do the testing. This should be a creative programmer different from the code developer.

2. Decide when testing will end.

3. Select what is to be measured. What will the goal of the test be?

4. Decide how the test will be conducted.

5. Develop the test cases. Test cases should be associated to use cases.

6. Execute the test cases using a test harness.

7. Carefully compare the results with the test oracle.

Glossary

black-box testing

Same as functional analysis.

boundary testing

Testing the system using inputs at or beyond their expected ranges.

error

A programmer mistake.

failure

Incorrect behavior resulting from a bug.

fault

Bug.

glass-box testing

Same as structural analysis.

quality assurance

regression testing

Re-testing components and collaborations after changes have been made to the system.

stress testing

Testing a system under extreme work loads.

verification

Are we building the product correctly?

validation

Are we building the correct product?

white-box testing

Same as structural analysis.

References

Chapter 12 of Software Engineering: An Engineering Approach, by Peters & Pedrycz; Wiley, 2000.

Chapter 5 of Classical and Object-Oriented Software Engineering ed. 4, by Schach; McGraw Hill, 1998.

Test Harness Example

// account.h

#include <iostream>
#include <iomanip>
#include <cassert>
using namespace std;

#define DEBUG_MODE
// #undef DEBUG_MODE

class Account
{
public:

   Account(double amt = 0)
   {
      #ifdef DEBUG_MODE
      cout << "entering Account(), amt = " << amt << endl;
      #endif

      assert(0 <= amt);

      balance = amt;

      #ifdef DEBUG_MODE
      cout << "exiting Account()\n";
      #endif

   }

   virtual ~Account()
   {
      #ifdef DEBUG_MODE
      cout << "entering ~Account()\n";
      #endif

      #ifdef DEBUG_MODE
      cout << "exiting ~Account()\n";
      #endif
   }

   void withdraw(double amt)
   {
      #ifdef DEBUG_MODE
      cout << "entering withdraw(), amt = " << amt << endl;
      #endif

      assert(0 <= amt);

      if (amt <= balance)
         balance -= amt;
      else
         cerr << "Sorry, insufficient funds\n";

      #ifdef DEBUG_MODE
      cout << "exiting withdraw()\n";
      #endif
   }

   void deposit(double amt)
   {
      #ifdef DEBUG_MODE
      cout << "entering deposit(), amt = " << amt << endl;
      #endif

      assert(0 <= amt);

      balance += amt;

      #ifdef DEBUG_MODE
      cout << "exiting deposit()\n";
      #endif
   }

   double getBalance() const { return balance; }

   // unit test harness
   static void test(ostream& log = cout);

private:
   double balance;
};

ostream& operator<<(ostream& os, const Account& acct);

// account.cpp

#include "account.h"

ostream& operator<<(ostream& os, const Account& acct)
{
   os.setf(ios::fixed, ios::floatfield);
   os << setprecision(2); // try a manipulator
   os << "balance = $" << acct.getBalance() << endl;
   os.precision(6);
   os.setf(0, ios::floatfield); // reset
   return os;
}

void Account::test(ostream& log)
{
   log << "\nentering Account::test() ... \n";
   Account* a = new Account(20);
   a->deposit(50);
   log << *a;
   a->withdraw(30);
   log << *a;
   a->withdraw(100);
   log << *a;
   a->deposit(-50);
   log << *a;
   log << "exiting Account::test() ... \n";
};

// test.cpp

#include "account.h"

int main()
{
   Account::test();

   return 0;
}

Test Harness Output