Like all engineering disciplines, software engineering relies on a reuse discipline to achieve economies of scale, improved product quality, shorter production cycles, and reduced process risk. But contrary to all other engineering disciplines, reuse is very difficult in software engineering and has been found to be effective only within the confines of product line engineering (PLE). The purpose of this chapter is to briefly introduce the reader to the reuse discipline of PLE and to focus specifically on testing software products produced and evolved within this discipline.
Given that software is very labor-intensive (hence hard to automate), that software labor is very expensive (as it requires a great deal of specialized expertise), and that software products are very hard to produce (due to their size and complexity), one would expect software reuse to be an indispensable component of software engineering. Indeed, for an industry that is under as much stress as the software industry, reuse offers a number of significant advantages, including the following:
Despite all these advantages, software reuse has not caught on as a general routine practice in software engineering, for a number of reasons, chief among them is the absence of a reference architecture in software products. Indeed, in order for reuse to happen, the party that produces reusable assets and the party that consumes them need to have a shared product architecture in mind. In the automobile industry, for example, component reuse has been a routine practice for over a century because the basic architecture of automobiles has not changed since the late nineteenth century: All cars are made up of a chassis, four wheels, a cab, an engine, a transmission, a steering mechanism, a braking mechanism, a battery, an electric circuitry, a fuel tank, an exhaust pipe, an air-conditioning system, and so on. As a result of this standard architecture, many industries emerge around the production of specific parts of this architecture, such as tire manufacturers, battery manufacturers, transmission manufacturers, exhaust manufacturers, and air-conditioning manufacturers, as well as other more specialized (and less visible to the average user) auto parts manufacturers. This standard architecture supports reuse on a large scale: when they design a new automobile, car manufacturers do not reinvent what is a car every time; rather, they may make some design decision pertaining to look, styling, engine performance characteristics, standard features, and optional features, then pick up their phone and order all the auto parts that they envision for their new car. Unfortunately, such an efficient/flexible process is not possible for software products, for lack of a standard architecture for such products.
But while software products have no common architecture across the broad range of applications where software is prevalent, they may have a common architecture within specific application domains—whence PLE. PLE is a software development and evolution process that represents a streamlined form of software reuse. It is geared toward the production of a family of related software products within a particular application domain and includes two major phases, which are as follows:
As a simple illustration, consider a product line developed to cater to the IT needs of banks in some jurisdiction (e.g., the state of New Jersey in the United States).
Figure 16.1 illustrates the lifecycles of domain engineering and application engineering and how they relate to each other.
To get a sense of the issues that arise in testing product lines, we review the traditional lifecycle of software testing then we explore how this lifecycle can be combined with the PLE process discussed earlier (and illustrated in Fig. 16.1). Given a software product P and a specification R, and given that we want to test whether P is correct with respect to R, we proceed through the following phases:
Of course, we can carry out these steps for each application that we develop at the application engineering phase, but this raises the following issues:
In light of these issues, it is legitimate to consider shifting the bulk of testing to the domain engineering phase, rather than the application engineering phase. Unfortunately, this option raises a host of issues as well, such as the following:
There is no simple, integrated, widely accepted, solution to the problems raised earlier. Rather, there are general guidelines that one ought to pursue in designing a testing policy for any particular product line; we review these guidelines in this section and illustrate them on a simple example in the next section. These guidelines are driven by the following principles:
These principles are illustrated in the next section, through a sample product line, its implementation, and its test.
We wish to develop a product line of applications that simulate the behavior of waiting queues at service stations. These may represent customers standing in line at checkout counters at a store, travelers standing at airline check-in counters at an airport, arriving passengers standing at immigration stations in an international arrivals terminal, postal customers standing in line for service at a post office, or processes being queued at a shared resource allocation post. The purpose of the applications in this product line is to enable managers to simulate various queuing and servicing policies and analyze their performance in terms of waiting time, fairness, throughput, and so on.
Among the commonalities that we envision between all the applications of this product line, we cite the following:
Among the variabilities between applications in this product line, we cite the following:
As for how to place each customer in the selected queue, we consider two options: an FIFO policy or a priority-based policy.
In the next section, we consider a possible reference architecture for applications in this domain, then we implement some reusable/adaptable components of this architecture and outline how such components are composed to produce an application.
We have to make provisions for all possible configurations of the service stations and corresponding queues, namely, variable number of service stations, variable number of queues, variable number of service station types, various mappings from queues to service stations, and so on. In order to cater for all possible configurations, we resolve to introduce a basic building block, which we call the queue-station block; each such a block is made up of a number of interchangeable service stations and a single queue feeding customers to these stations. We represent such a block by the symbol QS(n), where n is the number of service stations and QS stands for queue-service station. We leave it to the reader to check that all possible queue/station configurations can be implemented by a set of such blocks, with varying values of n. For example, if we want to simulate the situation of an airline check in counter that has two stations for first class, three stations for business class, and five stations for coach, we write (in the style of a type-variable declaration) as follows:
QS(2) firstclass; QS(3) businessclass; QS(5) coach.
In addition to specifying the number of service stations in a QS component, we may want to also specify the queuing policy; if we want the first-class and business-class queues to adopt an FIFO policy but want to adopt a priority-based policy for coach queues (e.g., award some privileges to frequent flyers who still fly coach), we may write the following:
QS(FIFO, 2) firstclass; QS(FIFO, 3) businessclass; QS(PRIORITY, 5) coach.
If, for example, we want to simulate the situation of waiting queues at a gas station, where each pump has its own queue of cars and cars are served by order of arrival, then we write the following:
QS(FIFO, 1) pump1, pump2, pump3, pump4, pump5;
In addition to specifying the configuration of queue/station sets, we may also want to specify policies pertaining to how some service station may serve the queues of other service stations; for example, in an airline check-in counter, it is common for first-class stations to serve coach passengers if the station is free and the coach queue is not empty. To this effect, we use the feature CrossStation, and we consider the following options to this feature:
For example, in the case of an airline check-in counter, we may write the following:
To feed customers to the simulation, we create a component called Arrivals, which generates customers according to the arrival rate provided by the user at run time. This component implements the arrivals distribution of the application and is responsible for the implementation of the queuing policy, at least as far as dispatching arriving customers to QSs, according to their category or to some other criterion. We assume that the Arrivals component takes two parameters, which are as follows:
In addition to specifying where incoming customers are placed, we may also want to specify whether the assignment of customers to queues is permanent (until the customer is served) or whether a customer may jump from one queue to another. We use the feature CrossQueue to this effect, and we consider the following options to this feature:
We assume that CrossQueue transfers take place only within QS sets of the same category.
Also, to collect statistics pertaining to the simulation, we create a component called Statistics, that is called by the QSs whenever a customer is about to leave the system after being serviced; it may also be called by the QSs at each iteration if the user is interested in measuring the rate of occupancy of service stations. This component collects data about individual customers, then computes simulation-wide statistics at the end of the simulation and posts it to the user. We assume that this component lists as parameters all the statistics that are selected at application engineering time, including the following:
It is possible to envision an AML that we use to characterize each application of this domain. In addition to all the details specified earlier, the language may include an indication of whether the simulation ends abruptly when the simulation time runs out or whether it winds down smoothly until all residual customers have been serviced and leave the system. This can be written as follows:
Hence, for example, if we wanted to specify the simulation of waiting queues in a gas station, we would write the following:
Simulation gasStation
{
QS(FIFO, 1) pump1, pump2, pump3, pump4; // four gas pumps
CrossStation(NONE); // each pump services its own queue
Arrivals (MARKOV, ANY); // arrival law, random assignment to queues
CrossQueue(NONE); // each car stays in its queue
Statistics (WT, OR, MQL); // wait time, occupancy rate, maximum queue length
WrapUp (SMOOTH); // at closing, serve remaining cars
}
Ideally, one may want to define a formal AML, and build a compiler for it, in such a way that an application description such as this could be compiled into a finished application.
The foregoing discussion yields a natural reference architecture for the proposed product line, whose main components include instances of the queue/station structure (QS), an Arrivals component, a Statistics component, and a main program to coordinate all these; this is illustrated in Figure 16.2. This figure describes the dataflow between these components; as for the control flow, it is basically limited to the main component invoking all the others in a sequential manner.
We review the variabilities that we have listed in Section 16.4.1 and see how these map onto this architecture; in other words, once we decide on the value of a variability, we must determine which components must be modified and how. We consider the variabilities, in turn:
We choose to implement this product line in C++. In this section, we outline the broad structure of the main program, then we implement the main building blocks that are used in this product line. The main program reads as follows:
#include <iostream>line 1
#include “qs.cpp”2
#include “arrivals.cpp”3
#include “statistics.cpp”4
using namespace std;5
typedef int clocktimetype;6
typedef int durationtype;7
/* State Objects */8
qsclass qs1, qs2;9
arclass arrivals;10
stclass stats;11
/* State Variables */12
durationtype expduration;13
int arrivalrate1, servicerate1; // for class 114
int arrivalrate2, servicerate2; // for class 215
int nbcustomers;16
/* Working Variables */17
clocktimetype clocktime;18
customertype customer;19
bool newarrival;20
int locsum, locmin, locmax;21
/* functions */22
bool ongoingSimulation();23
void elicitParameters();24
int main ()25
{26
elicitParameters();27
while (ongoingSimulation())28
{29
arrivals.drawcustomer(clocktime, expduration,30
arrivalrate1, servicerate1, customer, newarrival);31
if (newarrival) {nbcustomers++; qs1.enqueue(customer);}32
arrivals.drawcustomer(clocktime, expduration,33
arrivalrate2, servicerate2, customer, newarrival);34
if (newarrival) {nbcustomers++; qs2.enqueue (customer);}35
qs1.update(clocktime, locsum, locmin, locmax);36
stats.record(locsum,locmin,locmax);37
qs2.update(clocktime, locsum, locmin, locmax);38
stats.record(locsum,locmin,locmax);39
clocktime++;40
};41
cout << “concluded at time: ” << clocktime << endl;42
stats.summary(nbcustomers);43
}44
bool ongoingSimulation()45
{46
return ((clocktime<=expduration) ||47
(!qs1.done()) || (!qs2.done()));48
};49
void elicitParameters()50
{51
nbcustomers=0;52
cout << “Length of Simulation” << endl;53
cin >> expduration;54
cout << “Arrival rate, Service Rate, Station 1” << endl;55
cin >> arrivalrate1 >> servicerate1;56
cout << “Arrival rate, Service Rate, Station 2” << endl;57
cin >> arrivalrate2 >> servicerate2;58
};59
As written, this main program refers to two identical QS components; but in general, it may refer to more than one type of QS components (as we recall, QS components may differ by their number of stations and their queuing policy). Also, this main program refers to an arrivals component, that determines the rate of customer arrivals, and a statistics component, that collects statistics. For the sake of simplicity, we have opted for a straightforward (tree-like) #include hierarchy between the various components of this program; the price of this choice is that most data has to transfer through the main program rather than directly between the subordinate components. Hence the decision of whether there is a new arrival and the selection of the new customer parameters transits through the main program (lines 30–31 and 32–34) on its way to the QS component that stores incoming customers (lines 32 and 35); likewise, statistical data is sent from the QS components (lines 36 and 38) to the statistics components (lines 37 and 39) via the main program. Note that while the topology and configuration of the queues and service stations is decided at application engineering time, the actual simulation parameters (experiment duration, arrival rate of each class of customers, service rate of each class of customers) are decided at run-time (line 27).
The QS component is defined by the following header file:
//********************************************************
// Header file qs.h
//
//********************************************************
const int maxq = 1000; // max size of queueline 1
const int nbs = 3; // number of stations for single queue2
const int largewait=2000; // used for min wait3
typedef int clocktimetype;4
typedef int servicetimetype;5
typedef int durationtype;6
typedef int customeridtype;7
typedef int indextype;8
typedef struct9
{customeridtype cid;10
clocktimetype at;11
servicetimetype st;12
int ccat;13
} customertype;14
typedef struct15
{customertype guest;16
durationtype busytime;17
int busyrate;18
} stationtype;19
class qsclass20
{public:21
qsclass (); // default constructor22
bool done ();23
void update (clocktimetype clocktime,24
int& locsum, int& locmin, int& locmax);25
bool emptyq () const; // tells whether q is empty26
void enqueue (customertype qitem);27
void dequeue ();28
void checkfront (customertype& qitem) const;29
int queuelength ();30
31
private:32
customertype qarray [maxq];33
stationtype sarray [nbs];34
indextype front;35
indextype back;36
int qsize;37
};38
The nbs parameter in this header file (line 2) indicates the number of service stations in the QS component; ideally, we would like to define a single QS component for each queuing policy (e.g., FIFO), and let nbs be a parameter (hence, e.g., writing QS(3) or QS(5) depending on the number of stations we want to have for each queue) but we do not believe C++ allows that; hence in practice we write a separate QS class for each different value of nbs and each different value of the queuing policy; in the case of this product line, if we adopt FIFO as the only queuing policy and as the only viable number of stations per queue, then only one QS class is needed. The state variables of this class include the queue infrastructure (qarray, front, back, qsize) as well as an array of service stations, of size nbs. In addition to the queue methods (emptyq, queuelength, checkfront, enqueue, dequeue), this class has two QS-specific methods, which are (Boolean-valued) done() and (void) update(clocktime, locsum, locmin, locmax). The former indicates that the QS component has no residual customers in its queue or its service stations; the latter updates the queue and service stations on the grounds that a new unit of time (minute, second, millisecond, etc.) has elapsed:
The arrivals component reads as follows:
****************************************************line 1
//2
// arrivals component;3
// file arrivals.cpp, refers to header file arrivals.h.4
//5
//******************************************************6
#include “arrivals.h” 7
#include “rand.cpp”8
arclass :: arclass ()9
{SetSeed(673); customerid=1001;10
};11
void arclass :: drawcustomer (clocktimetype clocktime, int expduration,12
int arrivalrate, int servicerate,13
customertype& customer, bool& newarrival)14
{float draw = NextRand();15
newarrival = ((clocktime<=expduration) && 16
(draw<(1.0/float(arrivalrate))));17
if (newarrival)18
{customer.cid = customerid; customerid = customerid+3;19
customer.at = clocktime;20
customer.st = 1+int(NextRand()*servicerate); 21
}22
}23
This component calls a random number generator using the parameters of arrival time to determine whether or not there is an arrival at time clocktime, and if there is an arrival, it uses the parameter of service time to draw the length of service needed by the new arriving customer, assigns it a customer ID, and timestamps its arrival time. This information is used subsequently for reporting purposes and/ or to compute statistics.
The header of the statistics component reads as follows:
//*********************************************** line 1
// Header file statistics.h 2
// 3
//*********************************************** 4
class stclass 5
{public: 6
stclass (); // default constructor 7
void summary (int nbcustomers); 8
void record (int locsum, int locmin, int locmax); 9
private: 10
int totalwait, minwait, maxwait; 11
int totalstay, minstay, maxstay; 12
}; 13
As written, this component maintains some information about wait times and stay times of customers in the system; it is adequate if all we are interested in are statistics about these two quantities; but it needs to be expanded if we are to support all the variabilities listed in Section 16.4.1. As written, this component has two main functions, which are as follows:
In order to test the product line commonalities at domain engineering, we can proceed in one of the following two ways:
Focusing on the first approach, we propose to consider individual components, pin down their specification as precisely as possible to derive their test oracle, build dedicated test drivers for them, and test them to an arbitrary level of thoroughness, as a way to gain confidence in the correctness and soundness of the product line assets.
As an illustration, we consider, for example, the arrivals components, and write a test driver for it, in such a way that its behavior can be checked easily. For example, if we invoke the arrivals component 10,000 times with an arrival rate of 4 and a service rate of 20, then we expect to generate about 2,500 new customers whose average service rate is about 10 units of time. Note that to write this test driver, we need not see the file arrivals.cpp (in fact it is better not to, for the sake of redundancy); we only need to see the (specification) file arrivals.h. We propose the following test driver:
#include <iostream> line 1
#include “qs.cpp” 2
#include “arrivals.cpp” 3
using namespace std; 4
typedef int clocktimetype; 5
typedef int durationtype; 6
/* State Objects */ 7
arclass arrivals; 8
/* Working Variables */ 9
customertype customer; bool newarrival; 10
int nbcustomers; durationtype totalst; 11
int main () 12
{for (int clocktime=1; clocktime<=10000;
clocktime++)13
{arrivals.drawcustomer(clocktime,10000,4,20,
customer,newarrival); 14
if (newarrival) {nbcustomers++;
totalst=totalst+customer.st;} 15
}; 16
cout << “nb customers: ” << nbcustomers << endl; 17
cout << “average service time: ” << float(totalst)/
nbcustomers << endl;18
} 19
Execution of this test driver yields the following output:
nb customers: 2506
average service time: 10.7027
which corresponds to our expectation and enhances our faith in the arrivals component.
We could, likewise, test the QS component by generating and storing a number of customers in its queue, then monitoring how it handles the load. For example, we can generate 300 customers that each requires 20 units of service time, and see to it that it schedules them in 2000 minutes. We consider the following test driver:
#include <iostream> line 1
#include “qs.cpp” 2
#include “statistics.cpp” 3
using namespace std; 4
5
typedef int clocktimetype; 6
typedef int durationtype; 7
8
/* State Objects */ 9
qsclass qs; 10
stclass stats; 11
/* Working Variables */ 12
clocktimetype clocktime; 13
customertype customer; 14
durationtype expduration; int nbcustomers; 15
int locsum, locmin, locmax; 16
/* functions */ 17
bool ongoingSimulation(); 18
19
int main () 20
{clocktime=0; 21
expduration = 0; // terminate whenever qs is empty 22
customer.cid=1001; customer.at=0; customer.st=20; 23
for (int i=1; i<=300; i++) {qs.enqueue(customer);}; 24
nbcustomers=300; 25
while (ongoingSimulation()) 26
{qs.update(clocktime, locsum, locmin, locmax); 27
stats.record(locsum,locmin,locmax); 28
clocktime++; 29
}; 30
cout << “concluded at time: ” << clocktime << endl; 31
stats.summary(nbcustomers); 32
} 33
bool ongoingSimulation() 34
{return ((clocktime<=expduration) ||(!qs.done())); 35
}; 36
The outcome of the execution of this test driver is the following output, which corresponds to our expectation: The 300 customers kept the 3 stations busy nonstop for a total of 100 × 20 minutes, that is, 2000 minutes. The sum of waiting times is an arithmetic series, of the form
Dividing this sum by the number of customers on each station (100) and replacing the arithmetic series by its closed form expression, we find
The minimum waiting time is 0, of course since the stations were available at the start of the experiment. The maximum waiting time is the waiting time of the last customer in each queue, which had to wait for the 99 customers before it, hence the maximum waiting time is 99 × 20 = 1980. All this is confirmed in the following output, delivered by the test driver (except for the minor detail that the test driver shows the closing time at 2001 rather than 2000, but that is because the main loop increments the clocktime at the bottom of the loop body).
concluded at time: 2001
nb customers 300
statistics, wait time (total, avg, min, max):
297000 990 0 1980
Interestingly, running this test driver enabled us to uncover and remove a fault in the code of the QS component, which was measuring stay time rather than wait time. As far as domain engineering is concerned, we can perform testing under the following conditions:
In our case study, we have tested component qs with ; we can be reasonably confident that changing the value of nbs for the purposes of another application does not alter significantly the confidence we have in its correctness. But changing the queuing policy, however, (e.g., from FIFO to priority) will require a new testing effort. We are able to single out individual components, write test drivers for them, and design targeted test data that will exercise specific functionalities, and for which we know exactly what outcome to expect.
All the testing effort that we carry out at the domain engineering phase is expended once, but will benefit every application that is produced from this product line. While the test we have conducted so far, and other tests focused on system components one at a time, give us confidence in the correctness of the individual components, they give us no confidence in the soundness of the architecture, nor in the integration of the components to form an application.
One way to test the architecture is to run experiments that exercise the interactions between different components of the architecture. Consider, for example, the interaction between two queue-station components in the context of a CrossStation() relation. We assume that the queue-station components are declared by the following AML statements:
QS(3) coach; QS(2) firstclass;
CrossStation(firstclass, coach);
Wrap-Up(Abrupt);
Then one way to test the interaction between the two queue-station components is to run the simulation with far more coach passengers than the coach service station can serve, and virtually no first passengers at all, and observe that the simulation proceeds as though we had a single coach queue for five coach stations. A sample of data that makes it possible is as follows:
Then upon termination of the simulation, we may find that all five workstations were busy virtually 100% of the time.
In the application engineering phase, we take the domain engineering assets and use them to build an application on the basis of specific requirement specifications. In application engineering, we avail ourselves of an executable product for which we have a product specification; hence we have everything we need to run a test; the issue here is to maximize return on investment by targeting test data to those aspects of the application that have not been adequately tested at domain engineering or have not been adequately covered by the test of other applications within the same domain. Also, we must test that the variabilities are bound correctly with respect to the AML specification.
We consider a sample application in our queue simulation domain, specified by the following AML statements:
Simulation airlineCheckin
{
QS(FIFO, 4) coach;
QS(FIFO, 2) firstClass;
CrossStation(NONE); // each class services its own queue
Arrivals (UNIFORM, CAT); // arrival law, assignment by category
CrossQueue(NONE); // each passenger stays in his/her queue
Statistics (WT); // wait time
WrapUp (SMOOTH); // when check-in ends, take no new
// passengers, but clear lines
}
In light of this specification, we propose to define two QS classes, one with four service stations, and one with two service stations. Also, we envision that the user specifies the arrival rate and service rate of each class of service (coach, first class), and to deliver statistics about wait times. This yields the following simulation program.
#include <iostream>
#include “qs2.cpp”
#include “qs4.cpp”
#include “arrivals.cpp”
#include “statistics.cpp”
using namespace std;
typedef int clocktimetype;
typedef int durationtype;
/* State Objects */
qsclass2 qs2;
qsclass4 qs4;
arclass arrivals;
stclass stats;
/* State Variables */
durationtype expduration;
int arrivalrate2, servicerate2; // for class 2
int arrivalrate4, servicerate4; // for class 4
int nbcustomers;
/* Working Variables */
clocktimetype clocktime;
customertype customer;
bool newarrival, newdeparture;
int locsum, locmin, locmax;
/* functions */
bool ongoingSimulation();
void elicitParameters();
int main ()
{elicitParameters();
while (ongoingSimulation())
{arrivals.drawcustomer(clocktime, expduration,
arrivalrate2, servicerate2, customer, newarrival);
if (newarrival) {nbcustomers++; qs2.enqueue(customer);}
arrivals.drawcustomer(clocktime, expduration,
arrivalrate4, servicerate4, customer,
newarrival);
if (newarrival) {nbcustomers++; qs4.enqueue
(customer);}
qs2.update(clocktime, locsum, locmin, locmax);
stats.record(locsum,locmin,locmax);
qs4.update(clocktime, locsum, locmin, locmax);
stats.record(locsum,locmin,locmax);
clocktime++;
};
cout << “concluded at time: ” << clocktime << endl;
stats.summary(nbcustomers);
}
bool ongoingSimulation()
{return ((clocktime<=expduration)||(!qs2.done())
||(!qs4.done()));
};
void elicitParameters()
{nbcustomers=0;
cout << “Length of Simulation” << endl;
cin >> expduration;
cout << “Arrival rate, Service Rate, First Class” << endl;
cin >> arrivalrate2 >> servicerate2;
cout << “Arrival rate, Service Rate, Coach” << endl;
cin >> arrivalrate4 >> servicerate4;
};
Successive executions of this program with different arrival rates and service rates give the following results:
Duration | First class | Coach | Time ended | Customers served | Average wait | Maximum wait | ||
Arrival rate | Service rate | Arrival rate | Service Rate | |||||
900 | 4 | 24 | 3 | 19 | 1573 | 542 | 152 | 665 |
900 | 4 | 20 | 3 | 15 | 1321 | 542 | 95 | 415 |
900 | 4 | 16 | 3 | 12 | 1076 | 542 | 41 | 173 |
900 | 4 | 14 | 2 | 7 | 987 | 688 | 13 | 92 |
900 | 4 | 10 | 2 | 5 | 906 | 688 | 0.89 | 13 |
1000 | 5 | 20 | 4 | 14 | 1086 | 465 | 21 | 82 |
This Table Shows The Test Data Used For the experiment, as well as the output produced by the simulation. For completeness, we need an oracle that tells us whether the output produced by the simulation is correct; because of the random nature of the simulation, the oracle is not a deterministic function but rather the mean of a random variable. Hence, in terms of an oracle, we need to build an analytical model that shows the expected results of the simulation according to the parameters of the application (decided at application engineering time) and according to the parameters of the simulation (decided at run time). This model would be developed at domain engineering and applied for each application to serve as a test oracle.
This chapter highlights the difficulty of testing software product lines, due to unbound specifications and to the combinatorial explosion that stems from multiple variabilities, and proposes some general principles to guide the testing process, which are as follows:
Simulation gasStation
{
QS(FIFO, 1) pump1, pump2, pump3, pump4;
CrossStation(NONE); // each pump services its own queue
Arrivals (UNIFORM); // arrival law
CrossQueue(NONE); // each car stays in its queue
Statistics (WT); // wait time
WrapUp (ABRUPT); // when the station closes, the pumps stop
}
Develop a reference application according to the AML specification and use it to test the commonalities of the product line.
The field of software product line testing is still in its infancy; while many researchers agree on the broad challenges facing the discipline, there is no consensus on a general solution. John McGregor (2001) was the first to draw attention to the unique nature and unique challenges of product line testing. Recent surveys of the field include Machado et al. (2014) and Engstrom and Runeson (2011). The sample example of the queue simulation product line is due to Mili et al. (2002). The domain engineering methodology adopted in this chapter is FAST, which is due to Weiss and Lai (1999). For more on software product lines, consult Weiss and lai (1999), Pohl et al. (2005), or Linden et al. (2007). Another source is the annual Software Product Line Conference(s) (SPLC) conferences at http://www.splc.net/.