ProJPA-Skripta-Napredne softverske tehnologije-Informatika_4, Skripte' predlog Programiranje složenih softverskih sistema
dcplover
dcplover

ProJPA-Skripta-Napredne softverske tehnologije-Informatika_4, Skripte' predlog Programiranje složenih softverskih sistema

88 str.
578broj poseta
Opis
ProJPA,Skripta,Napredne softverske tehnologije,Informatika, Enterprise Applications,Object-Relational Mapping,Collection Mapping, Entity Manager,Using Queries, Query Language, Criteria API, Advanced Object-Relational Mapping, Advanced Topics, XML Mapping Files,Packaging and Deployment, Testing,Migration
20 poeni
poeni preuzimanja potrebni da se preuzme
ovaj dokument
preuzmi dokument
pregled3 str. / 88
ovo je samo pregled
3 prikazano na 88 str.
ovo je samo pregled
3 prikazano na 88 str.
ovo je samo pregled
3 prikazano na 88 str.
ovo je samo pregled
3 prikazano na 88 str.

CHAPTER 13 ■ PACKAGING AND DEPLOYMENT

423

Specifying Properties at Runtime One of the benefits of running outside the server is the ability to specify provider properties at runtime. This is available because of the overloaded createEntityManagerFactory() method that accepts a Map of properties in addition to the name of the persistence unit. The properties passed to this method are combined with those already specified, normally in the persistence.xml file. They may be additional properties or they may override the value of a property that was already specified. This may not seem very useful to some applications, since putting runtime configuration information in code is not normally viewed as being better than isolating it in an XML file. However, one can imagine this being a convenient way to set properties obtained from a program input, such as the command line, as an even more dynamic configuration mechanism. In Listing 13-16 is an example of taking the user and password properties from the command line and passing them to the provider when creating the EntityManagerFactory.

Listing 13-16. Using Command-Line Persistence Properties

public class EmployeeService { public static void main(String[] args) { Map props = new HashMap(); props.put("javax.persistence.jdbc.user", args[0]); props.put("javax.persistence.jdbc.password", args[1]); EntityManagerFactory emf = Persistence .createEntityManagerFactory("EmployeeService", props); // ... emf.close(); } }

System Classpath In some ways, configuring a persistence unit in a Java SE application is actually easier than configuring in the server because the classpath is simply the system classpath. Adding classes or jars on the system classpath is a trivial exercise. In the server we may have to manipulate the manifest classpath or add some vendor-specific application classpath configuration.

Schema Generation When we mentioned schema generation in Chapter 4, we promised to go over the mapping annotation elements that are considered when schema generation occurs. In this section, we will make good on that pledge and explain which elements get applied to the generated schema for those vendors that support schema generation.1

A couple of comments are in order before we start into them, though. First, the elements that contain the schema-dependent properties are, with few exceptions, in the physical annotations. This is

1 Most JPA vendors support some kind of schema generation either in the runtime or in a tool.

Download at WoweBook.Com

CHAPTER 13 ■ PACKAGING AND DEPLOYMENT

424

to try to keep them separate from the logical non-schema-related metadata. Second, these annotations are ignored, for the most part,2 if the schema is not being generated. This is one reason why using them is a little out of place in the usual case, since schema information about the database is of little use once the schema has been created and is being used.

One of the complaints around schema generation is that you can’t specify everything that you need to be able to finely tune the schema. This was done on purpose. There are too many differences between databases and too many different settings to try to put in options for every database type. If every database-tuning option were exposed through JPA then we would end up duplicating the features of Data Definition Language (DDL) in an API that was not meant to be a database schema generation facility. As we mentioned earlier, the majority of applications find themselves in a meet-in-the-middle mapping scenario in any case, and when they do have control over the schema, the final schema will typically be tuned by a database administrator or someone with the appropriate level of database experience.

Unique Constraints A unique constraint can be created on a generated column or join column by using the unique element in the @Column or @JoinColumn annotations. There are not actually very many cases where this will be necessary because most vendors will generate a unique constraint when it is appropriate, such as on the join column of one-to-one relationships. Otherwise, the value of the unique element defaults to false. Listing 13-17 shows an entity with a unique constraint defined for the STR column.

Listing 13-17. Including Unique Constraints

@Entity public class Employee { @Id private int id; @Column(unique=true) private String name; // ... }

Note that the unique element is unnecessary on the identifier column because a primary key constraint will always be generated for the primary key.

A second way of adding a unique constraint is to embed one or more @UniqueConstraint annotations in a uniqueConstraints element in the @Table or @SecondaryTable annotations. Any number of unique constraints may be added to the table definition, including compound constraints. The value passed to the @UniqueConstraint annotation is an array of one or more strings listing the column names that make up the constraint. Listing 13-18 demonstrates how to define a unique constraint as part of a table.

2 The exception to this rule may be the optional element of the mapping annotations, which may result in a NON NULL constraint, but which may also be used in memory to indicate that the value is or isn’t allowed to be set to null.

Download at WoweBook.Com

CHAPTER 13 ■ PACKAGING AND DEPLOYMENT

425

Listing 13-18. Unique Constraints Specified in Table Definition

@Entity @Table(name="EMP", uniqueConstraints=@UniqueConstraint(columnNames={"NAME"})) public class Employee { @Id private int id; private String name; // ... }

Null Constraints Constraints on a column may also be in the form of null constraints. A null constraint just indicates that the column may or may not be null. It is defined when the column is declared as part of the table.

Null constraints are defined on a column by using the nullable element in the @Column or @JoinColumn annotations. A column allows null values by default, so this element really needs to be used only when a value for the field or property is required. Listing 13-19 demonstrates how to set the nullable element of basic and relationship mappings.

Listing 13-19. Null Constraints Specified in Column Definitions

@Entity public class Employee { @Id private int id; @Column(nullable=false) private String name; @ManyToOne @JoinColumn(nullable=false) private Address address; // ... }

String-Based Columns When no length is specified for a column that is being generated to store string values, the length will be defaulted to 255. When a column is generated for a basic mapping of a field or property of type String, char[], or Character[], its length should be explicitly listed in the length element of the @Column annotation if 255 is not the desired maximum length. Listing 13-20 shows an entity with explicitly specified lengths for strings.

Listing 13-20. Specifying the Length of Character-Based Column Types

@Entity public class Employee { @Id @Column(length=40) private String name; @ManyToOne @JoinColumn(name="MGR")

Download at WoweBook.Com

CHAPTER 13 ■ PACKAGING AND DEPLOYMENT

426

private Employee manager; // ... }

We can see from the previous example that there is no similar length element in the @JoinColumn annotation. When primary keys are string-based, the provider may set the join column length to the same length as the primary key column in the table that is being joined to. This is not required to be supported, however.

It is not defined for length to be used for large objects; some databases do not require or even allow the length of lobs to be specified.

Floating Point Columns Columns containing floating point types have a precision and scale associated with them. The precision is just the number of digits that are used to represent the value, and the scale is the number of digits after the decimal point. These two values may be specified as precision and scale elements in the @Column annotation when mapping a floating point type. Like other schema generation elements, they have no effect on the entity at runtime. Listing 13-21 demonstrates how to set these values.

Listing 13-21. Specifying the Precision and Scale of Floating Point Column Types

@Entity public class PartTimeEmployee { // ... @Column(precision=8, scale=2) private float hourlyRate; // ... }

TIP Precision may be defined differently for different databases. In some databases and for some floating point types it is the number of binary digits, while for others it is the number of decimal digits.

Defining the Column There may be a time when you are happy with all the generated columns except for one. The type of the column isn’t what you want it to be, and you don’t want to go through the trouble of manually generating the schema for the sake of one column. This is one instance when the columnDefinition element comes in handy. By hand-rolling the DDL for the column, we can include it as the column definition and let the provider use it to define the column.

The columnDefinition element is available in all the column-oriented annotation types, including @Column, @JoinColumn, @PrimaryKeyJoinColumn, @MapKeyColumn, @MapKeyJoinColumn, @OrderColumn, and @DiscriminatorColumn. Whenever a column is to be generated, the columnDefinition element may be used to indicate the DDL string that should be used to generate the type (not including the trailing comma). This gives the user complete control over what is generated in the table for the column being

Download at WoweBook.Com

CHAPTER 13 ■ PACKAGING AND DEPLOYMENT

427

mapped. It also allows a database-specific type or format to be used that may supersede the generated type offered by the provider for the database being used.3 Listing 13-22 shows some definitions specified for two columns and a join column.

Listing 13-22. Using a Column Definition to Control DDL Generation

@Entity public class Employee { @Id @Column(columnDefinition="NVARCHAR2(40)") private String name; @Column(name="START_DATE", columnDefinition="DATE DEFAULT SYSDATE") private java.sql.Date startDate; @ManyToOne @JoinColumn(name="MGR", columnDefinition="NVARCHAR2(40)") private Employee manager; // ... }

In this example, we are using a Unicode character field for the primary key and then also for the join column that refers to the primary key. We also define the date to be assigned the default current date at the time the record was inserted (in case it was not specified).

Specifying the column definition is quite a powerful schema generation practice that allows overriding of the generated column to an application-defined custom column definition. But the power is accompanied by some risk as well. When a column definition is included, other accompanying column-specific generation metadata is ignored. Specifying the precision, scale, or length in the same annotation as a column definition would be both unnecessary and confusing.

Not only does using columnDefinition in your code bind you to a particular schema but it also binds you to a particular database since the DDL tends to be database-specific. This is just a flexibility- portability trade-off, and you have to decide whether it is appropriate for your application.

Summary It is a simple exercise to package and deploy persistence applications using the Java Persistence API. In most cases, it is just a matter of adding a very short persistence.xml file to the JAR containing the entity classes.

In this chapter, we described how to configure the persistence unit in the Java EE server environment using the persistence.xml file and how in some cases the name may be the only setting required. We then explained when to apply and how to specify the transaction type, the persistence provider, and the data source. We showed how to use and specify the default orm.xml mapping file and then went on to use additional mapping files within the same persistence unit. We also discussed the various ways that classes may be included in the persistence unit and how to customize the persistence unit using standard and vendor-specific properties.

3 The resulting column must be supported by the provider runtime to enable reading from and writing to the column.

Download at WoweBook.Com

CHAPTER 13 ■ PACKAGING AND DEPLOYMENT

428

We looked at the ways that persistence units may be packaged and deployed to a Java EE application as part of an EJB archive, a web archive, or a persistence archive that is accessible to all the components in the application. We examined how persistence units may exist within different scopes of a deployed Java EE application and what the name-scoping rules were. We then compared the configuration and deployment practices of deploying an application to a Java SE environment.

Finally, we showed how a schema can be generated in the database to match the requirements of the persistence unit using the domain model and the mapping metadata. We cautioned against using the generated schema for production systems, but showed how it can be used during development, prototyping, and testing to get up and running quickly and conveniently.

In the next chapter, we will consider the accepted and best practices for testing applications that use persistence.

Download at WoweBook.Com

C H A P T E R 14

■ ■ ■

429

Testing

One of the major selling points of JPA and EJB 3.0 has been the drive toward better testability. The use of plain Java classes where possible as well as the ability to use persistence outside of the application server has made enterprise applications much easier to test. This chapter will cover unit testing and integration testing with entities, with a mix of modern and traditional test techniques.

Testing Enterprise Applications Testing is generally accepted as being a good thing, but how exactly should we go about doing it? Almost all enterprise applications are hosted in some kind of server environment, whether it is a servlet container like Apache Tomcat or a full Java EE application server. Once deployed to such an environment, the developer is much more isolated from the application than if he was developing in a Java SE runtime environment. At this point, it can be tested only using the public interface of the application, such as a browser using HTTP, web service, RMI, or a messaging interface.

This presents an issue for developers because to do unit testing we want to be able to focus on the components of an application in isolation. An elaborate sequence of operations through a web site may be required to access a single method of a session bean that implements a particular business service. For example, to view an Employee record, a test client might have to log in using a user name and password, traverse several menu options, execute a search, and then finally access the record. Afterward, the HTML output of the report must be verified to ensure that the operation completed as expected. In some applications this procedure may be short-circuited by directly accessing the URL that retrieves a particular record. But with more and more information cached in HTTP session state, URLs are beginning to look like random sequences of letters and numbers. Getting direct access to a particular feature of an application may not be easy to achieve.

Java SE clients (so called “fat” clients) that communicate with databases and other resources suffer from the same problem despite their ability to execute the program without the need for an application server. The user interface of a Java SE client may well be a Swing application requiring special tools to drive it in order to do any kind of test automation. The application is still just a black box without any obvious way to get inside.

Numerous attempts have been made to expose the internals of an application to testing while deployed on a server. One of the first was the Cactus1 framework, which allows developers to write tests using JUnit, which are then deployed to the server along with the application and executed via a web

1 Visit http://jakarta.apache.org/cactus/ for more information.

Download at WoweBook.Com

CHAPTER 14 ■ TESTING

430

interface provided by Cactus. Other frameworks adopted a similar approach using RMI instead of a web interface to control the tests remotely.

Although effective, the downside to these approaches is that the application server still has to be up and running before we can attempt any kind of testing. For developers who use test-driven development (TDD), in which tests are written before code and the full unit test suite is executed after every development iteration (which can be as small as a change to a single method), any kind of interaction with the application server is a problem. Even for developers who practice a more traditional testing methodology, frequent test execution is hampered by the need to keep the application server running, with a packaging and deployment step before every test run.

Clearly, for developers who want to break a Java EE application into its component parts and test those components in isolation, there is a need for tools that will let us directly execute portions of the application outside of the server environment in which it is normally hosted.

Terminology Not everyone agrees about exactly what constitutes a unit test or an integration test. In fact, it is quite likely that any survey of a group of developers will yield a wide variety of results, some similar in nature while others venture into completely different areas of testing. Therefore we feel it is important to define our terminology for testing so that you can translate it into whatever terms you are comfortable with.

We see tests falling into the following four categories:

Unit tests. Unit tests are written by developers and focus on isolated components of an application. Depending on your approach, this may be a single class or a collection of classes. The only key defining elements in our opinion are that the unit test is not coupled to any server resources (these are typically stubbed out as part of the test process) and executes very quickly. It must be possible to execute an entire suite of unit tests from within an IDE and get the results in a matter of seconds. Unit test execution can be automated and is often configured to happen automatically as part of every merge to a configuration management system.

Integration tests. Integration tests are also written by developers and focus on use cases within an application. They are still typically decoupled from the application server, but the difference between a unit test and an integration test is that the integration test makes full use of external resources such as a database. In effect, an integration test takes a component from an application and runs in isolation as if it were still inside the application server. Running the test locally makes it much faster than a test hosted in an application server, but still slower than a unit test. Integration tests are also automated and often run at least daily to ensure that there are no regressions introduced by developers.

Functional tests. Functional tests are the black box tests written and automated by quality engineers instead of developers. Quality engineers look at the functional specification for a product and its user interface, and seek to automate tests that can verify product behavior without understanding (or caring) how the application is implemented. Functional tests are a critical part of the application development process, but it is unrealistic to execute these tests as part of the day- to-day work done by a developer. Automated execution of these tests often takes place on a different schedule, independent of the regular development process.

Download at WoweBook.Com

CHAPTER 4 ■ TESTING

431

Acceptance tests. Acceptance tests are customer-driven. These tests, usually conducted manually, are carried out directly by customers or representatives who play the role of the customer. The goal of an acceptance test is to verify that the requirements set out by the customer are fulfilled in the user interface and behavior of the application.

In this chapter, we will focus only on unit tests and integration tests. These tests are written by developers for the benefit of developers and constitute what is called white box testing. These tests are written with the full understanding of how the application is implemented and what it will take not only to test the successful path through an application but also to trigger failure scenarios.

Testing Outside the Server The common element between unit tests and integration tests is that they are executed without the need for an application server. Unfortunately for Java EE developers, this has traditionally been very difficult. Applications developed before the Java EE 5 release are tightly coupled to the application server, often making it difficult and counterproductive to attempt replicating the required container services in a stand-alone environment.

To put this in perspective, let’s look at Enterprise JavaBeans as they existed in EJB 2.1. On paper, testing a session bean class should be little more than a case of instantiating the bean class and invoking the business method. For trivial business methods, this is indeed the case, but things start to go downhill quickly once dependencies get involved. For example, let’s consider a business method that needs to invoke another business method from a different session bean.

Dependency lookup was the only option in EJB 2.1, so if the business method has to access JNDI to obtain a reference to the other session bean, either JNDI must be worked around or the bean class must be refactored so that the lookup code can be replaced with a test-specific version. If the code uses the Service Locator2 pattern, we have a bigger problem because a singleton static method is used to obtain the bean reference. The only solution for testing beans that use Service Locators outside the container is to refactor the bean classes so that the locator logic can be overridden in a test case.

Next we have the problem of the dependent bean itself. The bean class does not implement the business interface, so it cannot simply be instantiated and made available to the bean we are trying to test. Instead, it will have to be subclassed to implement the business interface, and stubs for a number of low-level EJB methods will have to be provided because the business interface in EJB 2.1 actually extends an interface that is implemented internally by the application server.

Even if we get that to work, what happens if we encounter a container-managed entity bean? Not only do we have the same issues with respect to the interfaces involved but the bean class is also abstract, with all the persistent state properties unimplemented. We could implement them, but our test framework would rapidly start to outgrow the application code. We can’t even just run them against the database as we can with JDBC code because so much of the entity bean logic, relationship maintenance, and other persistence operations are available only inside an EJB container.

The dirty secret of many applications written using older versions of Java EE is that there is little to no developer testing at all. Developers write, package, and deploy applications; test them manually through the user interface; and then hope that the quality assurance group can write a functional test that verifies each feature. It’s just too much work to test individual components outside of the application server.

2 Alur, Deepak, John Crupi, and Dan Malks. Core J2EE Patterns: Best Practices and Design Strategies, Second Edition. Upper Saddle River, N.J.: Prentice Hall PTR, 2003, p. 315.

Download at WoweBook.Com

CHAPTER 14 ■ TESTING

432

This is where EJB and JPA come in. Starting with EJB 3.0, a session bean class is a simple Java class that implements a regular Java interface, and in EJB 3.1 even the interface is no longer required for local beans. No special EJB interfaces need to be extended or implemented. To unit test the logic in a session bean, we can usually just implement it and execute it. If the bean depends on another bean, we can instantiate that bean and manually inject it into the bean being tested. The EJB 3.0 release was designed to encourage testing by breaking the hard dependencies between application code and the application server.

Likewise entities are a world apart from container-managed entity beans. If your session bean uses an entity, you can just instantiate it and use it like any other class. If you are testing code that uses the entity manager and want to verify that it is interacting with the database the way you expect it to, just bootstrap the entity manager in Java SE and make full use of the entity manager outside of the application server.

In this chapter, we will demonstrate how to take a session bean and JPA code from a Java EE application and run it outside the container using unit testing and integration testing approaches. If you have worked with older versions of EJB and experienced the pain of developer testing, prepare yourself for a completely different look at testing enterprise applications.

Test Frameworks The JUnit test framework is a de facto standard for testing Java applications. JUnit is a simple unit testing framework that allows tests to be written as Java classes. These Java classes are then bundled together and run in suites using a test runner that is itself a simple Java class. Out of this simple design, a whole community has emerged to provide extensions to JUnit and integrate it into all major development environments.

Despite its name, unit testing is only one of the many things that JUnit can be used for. It has been extended to support testing of web sites, automatic stubbing of interfaces for testing, concurrency testing, and performance testing. Many quality assurance groups now use JUnit as part of the automation mechanism to run whole suites of end-to-end functional tests.

For our purposes, we will look at JUnit in the context of its unit testing roots, and also at strategies that allow it to be used as an effective integration test framework. Collectively we look at these two approaches simply as developer tests because they are written by developers to assist with the overall quality and development of an application.

In addition to the test framework itself, there are other libraries that can assist with the testing of Java EE components. The EJB 3.1 release introduced the embedded EJB container, providing developers with many of the services of an EJB container without the requirement to run within an application server. Many nonstandard frameworks also offer sophisticated dependency injection support even in the Java SE environment, allowing dependent classes to be woven together. Even if a framework does not directly support EJB 3.0 annotations, the fact that session beans are simple Java classes makes them usable with any lightweight container framework. As always, before writing these kinds of frameworks for testing, check to see that the problem hasn’t already been solved. If nothing else, the Java community has shown a remarkable willingness to share solutions to problems in the open source community and even with commercial vendors.

We will assume that you are familiar with JUnit 4 (which makes use of annotations) at this point. Introductory articles and tutorials can be found on the JUnit website at http://www.junit.org. Many books and other online resources cover testing with JUnit in extensive detail.

Download at WoweBook.Com

CHAPTER 4 ■ TESTING

433

Unit Testing It might seem counterintuitive at first, but one of the most interesting things about entities is that they can participate in tests without requiring a running application server or live database. For years, enterprise developers have been frustrated with container-managed entity beans because they were effectively untestable without a live application server. The component and home interfaces could conceivably be used in unit tests, but only if the developer provided implementations of those interfaces, duplicating effort already invested in writing the real bean classes and potentially introducing new bugs in the process. Because entities are plain Java classes, they can be used directly in tests without any additional effort required.

In the following sections, we will look at testing entity classes directly and using entities as part of tests for Java EE components. We will also discuss how to leverage dependency injection in unit tests and how to deal with the presence of JPA interfaces.

Testing Entities Entities are unlikely to be extensively tested in isolation. Most methods on entities are simple getters or setters that relate to the persistent state of the entity or to its relationships. Business methods may also appear on entities, but are less common. In many applications, entities are little more than basic JavaBeans.

As a rule, property methods do not generally require explicit tests. Verifying that a setter assigns a value to a field and the corresponding getter retrieves the same value is not testing the application so much as the compiler. Unless there is a side effect in one or both of the methods, getters and setters are too simple to break and therefore too simple to warrant testing.

Key things to look for in determining whether or not an entity warrants individual testing are side effects from a getter or setter method (such as data transformation or validation rules) and the presence of business methods. The entity shown in Listing 14-1 contains nontrivial logic that warrants specific testing.

Listing 14-1. An Entity that Validates and Transforms Data

@Entity public class Department { @Id private String id; private String name; @OneToMany(mappedBy="department") private Collection<Employee> employees; public String getId() { return id; } public void setId(String id) { if (id.length() != 4) { throw new IllegalArgumentException( "Department identifiers must be four characters in length"); } this.id = id.toUpperCase(); } // ... }

Download at WoweBook.Com

CHAPTER 14 ■ TESTING

434

The setId() method both validates the format of the department identifier and transforms the string to uppercase. This type of logic and the fact that setting the identifier can actually cause an exception to be thrown suggests that tests would be worthwhile. Testing this behavior is simply a matter of instantiating the entity and invoking the setter with different values. Listing 14-2 shows one possible set of tests.

Listing 14-2. Testing a Setter Method for Side Effects

public class DepartmentTest { @Test public void testValidDepartmentId() throws Exception { Department dept = new Department(); dept.setId("NA65"); Assert.assertEquals("NA65", dept.getId()); } @Test public void testDepartmentIdInvalidLength() throws Exception { Department dept = new Department(); try { dept.setId("NA6"); Assert.fail("Department identifiers must be four characters"); } catch (IllegalArgumentException e) { } } @Test public void testDepartmentIdCase() throws Exception { Department dept = new Department(); dept.setId("na65"); Assert.assertEquals("NA65", dept.getId()); } }

Testing Entities in Components The most likely test candidate for entities is not the entity but the application code that uses the entity as part of its business logic. For many applications this means testing session beans, managed beans and other Java EE components. If the entities are obtained or initialized outside the scope of the component, testing is made easy in the sense that the entity class can simply be instantiated, populated with entity data and set into the bean class for testing. When used as a domain object in application code, an entity is no different from any other Java class. You can effectively pretend that it’s not an entity at all.

Of course, there is more to unit testing a session bean than simply instantiating entities to be used with a business method. We also need to be concerned with the dependencies that the session bean has in order to implement its business logic. These dependencies are usually manifested as fields on the bean class that are populated using a form of dependency injection or dependency lookup.

Download at WoweBook.Com

CHAPTER 4 ■ TESTING

435

When writing unit tests, our goal is to introduce the minimum set of dependencies required to implement a particular test. If we are testing a business method that needs to invoke a method on the EJBContext interface, we should worry only about providing a stubbed version of the interface. If the bean uses a data source but is not relevant to our testing, then ideally we want to ignore it entirely.

Dependency injection is the key to effective unit testing. By removing the JNDI API from session bean code and eliminating the need for the Service Locator pattern, you can ensure that the bean class has few dependencies on the application server. We need only instantiate the bean instance and manually inject the required resources, the majority of which will be either other beans from the application or test-specific implementations of a standard interface.

As we explained in Chapter 3, the setter injection form of dependency injection is the easiest to use in unit tests. Because the setter methods are almost always public, they can be invoked directly by the test case to assign a dependency to the bean class. Field injection is still easy to deal with so long as the field uses package scope because the convention for unit tests is to use the same package name as the class that is being tested.

When the dependency is another session bean, you must make a choice about whether all the dependencies of the required bean class must be met or whether a test-specific version of the business interface should be used instead. If the business method from the dependent business interface does not affect the outcome of the test, it may not be worth the effort to establish the full dependency. As an example, consider the session bean shown in Listing 14-3. We have shown a single method for calculating years of service for an employee that retrieves an Employee instance using the EmployeeService session bean.

Listing 14-3. Using the EmployeeService Bean in a Different Business Method

@Stateless public class VacationBean implements Vacation { public static final long MILLIS_PER_YEAR = 1000 * 60 * 60 * 24 * 365; @EJB EmployeeService empService; public int getYearsOfService(int empId) { Employee emp = empService.findEmployee(empId); long current = System.currentTimeMillis(); long start = emp.getStartDate().getTime(); return (int)((current - start) / MILLIS_PER_YEAR); } // ... }

Because the only thing necessary to verify the getYearsOfService() method is a single Employee instance with a start date value, there is no need to use the real EmployeeService bean. An implementation of the EmployeeService interface that returns an entity instance preconfigured for the test is more than sufficient. In fact, the ability to specify a well-known return value from the findEmployee() method makes the overall test much easier to implement. Listing 14-4 demonstrates using a test-specific implementation of a session bean interface. The implementation is defined as an anonymous class in the test class. Implementing an interface specifically for a test is called mocking the interface, and the instantiated instance is referred to as a mock object.

Download at WoweBook.Com

CHAPTER 14 ■ TESTING

436

Listing 14-4. Creating a Test-specific Version of a Business Interface

public class VacationBeanTest { @Test public void testYearsOfService() throws Exception { VacationBean bean = new VacationBean(); bean.empService = new EmployeeService() { public Employee findEmployee(int id) { Employee emp = new Employee(); emp.setStartDate(new Time(System.currentTimeMillis() - VacationBean.MILLIS_PER_YEAR * 5)); return emp; } // ... }; int yearsOfService = bean.getYearsOfService(0); Assert.assertEquals(5, yearsOfService); } // ... }

The Entity Manager in Unit Tests The EntityManager and Query interfaces present a challenge to developers writing unit tests. Code that interacts with the entity manager can vary from the simple (persisting an object) to the complex (issuing a JP QL query and obtaining the results). There are two basic approaches to dealing with the presence of standard interfaces:

• Introduce a subclass that replaces methods containing entity manager or query operations with test-specific versions that do not interact with JPA.

• Provide custom implementations of standard interfaces that may be predictably used for testing.

Before covering these strategies in detail, consider the session bean implementation shown in Listing 14-5 that provides a simple authentication service. For such a simple class, it is surprisingly challenging to unit test. The entity manager operations are embedded directly within the authenticate() method, coupling the implementation to JPA.

Listing 14-5. Session Bean that Performs Basic Authentication

@Stateless public class UserServiceBean implements UserService { @PersistenceContext(unitName="EmployeeService") EntityManager em; public User authenticate(String userId, String password) { User user = em.find(User.class, userId); if (user != null) { if (password.equals(user.getPassword())) {

Download at WoweBook.Com

CHAPTER 4 ■ TESTING

437

return user; } } return null; } }

The first technique we will demonstrate to make this class testable is to introduce a subclass that eliminates entity manager calls. For the UserServiceBean example shown in Listing 14-5, entity manager access must first be isolated to a separate method before it can be tested. Listing 14-6 demonstrates such a refactoring.

Listing 14-6. Isolating Entity Manager Operations for Testing

@Stateless public class UserServiceBean implements UserService { @PersistenceContext(unitName="EmployeeService") EntityManager em; public User authenticate(String userId, String password) { User user = findUser(userId); // ... } User findUser(String userId) { return em.find(User.class, userId); } }

With this refactoring complete, the authenticate() method no longer has any direct dependency on the entity manager. The UserServiceBean class can now be subclassed for testing, replacing the findUser() method with a test-specific version that returns a well-known result. Listing 14-7 demonstrates a complete test case using this technique.

Listing 14-7. Using a Subclass to Eliminate Entity Manager Dependencies

public class UserServiceTest { static final String USER_ID = "test_id"; static final String PASSWORD = "test_password"; static final String INVALID_USER_ID = "test_user"; @Test public void testAuthenticateValidUser() throws Exception { TestUserService service = new TestUserService(); User user = service.authenticate(USER_ID, PASSWORD); Assert.assertNotNull(user); Assert.assertEquals(USER_ID, user.getName()); Assert.assertEquals(PASSWORD, user.getPassword()); } @Test public void testAuthenticateInvalidUser() throws Exception { TestUserService service = new TestUserService(); User user = service.authenticate(INVALID_USER_ID, PASSWORD);

Download at WoweBook.Com

CHAPTER 14 ■ TESTING

438

Assert.assertNull(user); } class TestUserService extends UserServiceBean { private User user; public TestUserService() { user = new User(); user.setName(USER_ID); user.setPassword(PASSWORD); } User findUser(String userId) { if (userId.equals(user.getName())) { return user; } return null; } } }

This test case has the advantage of leaving the original authenticate() method implementation intact, only overriding the findUser() method for the test. This works well for classes that have been refactored to isolate persistence operations, but these changes cannot always be made. The alternative is to mock the EntityManager interface. Listing 14-8 demonstrates this approach.

Listing 14-8. Using a Mock Entity Manager in a Unit Test

public class UserServiceTest2 { static final String USER_ID = "test_id"; static final String PASSWORD = "test_password"; static final String INVALID_USER_ID = "test_user"; @Test public void testAuthenticateValidUser() throws Exception { UserServiceBean service = new UserServiceBean(); service.em = new TestEntityManager(USER_ID, PASSWORD); User user = service.authenticate(USER_ID, PASSWORD); Assert.assertNotNull(user); Assert.assertEquals(USER_ID, user.getName()); Assert.assertEquals(PASSWORD, user.getPassword()); } @Test public void testAuthenticateInvalidUser() throws Exception { UserServiceBean service = new UserServiceBean(); service.em = new TestEntityManager(USER_ID, PASSWORD); User user = service.authenticate(INVALID_USER_ID, PASSWORD); Assert.assertNull(user); } class TestEntityManager extends MockEntityManager { private User user;

Download at WoweBook.Com

CHAPTER 4 ■ TESTING

439

public TestEntityManager(String user, String password) { this.user = new User(); this.user.setName(user); this.user.setPassword(password); } public <T> T find(Class<T> entityClass, Object pk) { if (entityClass == User.class && ((String)pk).equals(user.getName())) { return (T) user; } return null; } } }

The advantage of this approach over subclassing is that it leaves the original bean class unchanged while allowing it to be unit tested. The MockEntityManager class referenced in the test is a concrete implementation of the EntityManager interface with empty method definitions. All methods that return a value return null or an equivalent instead. By defining it separately, it can be reused for other test cases. Many unit test suites contain a small set of mocked interfaces that can be reused across multiple tests.

TIP Check out http://www.mockobjects.com for further information on mock object techniques and open source tools to assist with mock object creation.

Integration Testing Integration testing, for our purposes, is an extension of unit testing that takes components of a Java EE application and executes them outside of an application server. Unlike unit testing, in which we went to great lengths to avoid the entity manager, in integration testing we embrace it and leverage the fact that it can be used in Java SE.

The following sections explore using JPA outside of an application server in order to test application logic with a live database, but without starting the application server. To better approximate the runtime environment, the same provider should be used for testing as is used in production.

Using the Entity Manager In Listing 14-5, we demonstrated a session bean that performed basic authentication against a User object retrieved from the database. To unit test this class, a number of techniques were presented to replace or mock the entity manager operation. The downside to this approach is that the test code required to work around external dependencies in the application code can quickly reach a point where it is difficult to maintain and is a potential source of bugs.

Instead of mocking the entity manager, a resource-local, application-managed entity manager may be used to perform tests against a live database. Listing 14-9 demonstrates a functional test version of the UserServiceBean test cases.

Download at WoweBook.Com

CHAPTER 14 ■ TESTING

440

Listing 14-9. Integration Test for UserServiceBean

public class UserServiceTest3 { static final String USER_ID = "test_id"; static final String PASSWORD = "test_password"; static final String INVALID_USER_ID = "test_user"; private EntityManagerFactory emf; private EntityManager em; @Before public void setUp() { emf = Persistence.createEntityManagerFactory("hr"); em = emf.createEntityManager(); createTestData(); } @After public void tearDown() { if (em != null) { removeTestData(); em.close(); } if (emf != null) { emf.close(); } } private void createTestData() { User user = new User(); user.setName(USER_ID); user.setPassword(PASSWORD); em.getTransaction().begin(); em.persist(user); em.getTransaction().commit(); } private void removeTestData() { em.getTransaction().begin(); User user = em.find(User.class, USER_ID); if (user != null) { em.remove(user); } em.getTransaction().commit(); } @Test public void testAuthenticateValidUser() throws Exception { UserServiceBean service = new UserServiceBean(); service.em = em; User user = service.authenticate(USER_ID, PASSWORD); Assert.assertNotNull(user);

Download at WoweBook.Com

CHAPTER 4 ■ TESTING

441

Assert.assertEquals(USER_ID, user.getName()); Assert.assertEquals(PASSWORD, user.getPassword()); } @Test public void testAuthenticateInvalidUser() throws Exception { UserServiceBean service = new UserServiceBean(); service.em = em; User user = service.authenticate(INVALID_USER_ID, PASSWORD); Assert.assertNull(user); } }

This test case uses the fixture methods setUp() and tearDown() to create EntityManagerFactory and EntityManager instances using the Java SE bootstrap API and then closes them when the test completes. The test case also uses these methods to seed the database with test data and remove it when the test completes. The tearDown() method is guaranteed to be called even if a test fails due to an exception. Like any JPA application in the Java SE environment, a persistence.xml file will need to be on the classpath in order for the Persistence class to bootstrap an entity manager factory. The file must contain the JDBC connection properties to connect to the database, and if the managed classes were not already listed, class elements would also need to be added for each managed class. If the transaction type was not specified, it will be defaulted to the correct transaction type according to the environment; otherwise, it should be set to RESOURCE_LOCAL. This example demonstrates the basic pattern for all integration tests that use an entity manager.

The advantage of this style of test versus a unit test is that no effort was required to mock up persistence interfaces. Emulating the entity manager and query engine in order to test code that interacts directly with these interfaces suffers from diminishing returns as more and more effort is put into preparing a test environment instead of writing tests. In the worst-case scenario, incorrect test results occur because of bugs in the test harness, not in the application code. Given the ease with which JPA can be used outside the application server, this type of effort may be better spent establishing a simple database test environment and writing automated functional tests.

However, despite the opportunity that testing outside the application server presents, care must be taken to ensure that such testing truly adds value. Quite often, developers fall into the trap of writing tests that do little more than test vendor functionality as opposed to true application logic. An example of this mistake is seeding a database, executing a query, and verifying that the desired results are returned. It sounds valid at first, but all that it tests is the developer’s understanding of how to write a query. Unless there is a bug in the database or the persistence provider, the test will never fail. A more valid variation of this test would be to start the scenario farther up the application stack by executing a business method on a session façade that initiates a query and then validating that the resulting transfer objects are formed correctly for later presentation by a JSP page.

Test Setup and Teardown Many tests involving persistence require some kind of test data in the database before the test can be executed. If the business operation does not create and verify the result of a persistence operation, the database must already contain data that can be read and used by the test. Because tests should ideally be able to set and reset their own test data before and after each test, we must have a way to seed the database appropriately.

This sounds pretty straightforward; use JDBC to seed the database during setUp() and again during tearDown() to reset it. But there is a danger here. Most persistence providers employ some kind of data or object caching. Any time data changes in the database without the persistence provider knowing about it, its cache will get out of sync with the database. In the worst-case scenario, this

Download at WoweBook.Com

CHAPTER 14 ■ TESTING

442

could cause entity manager operations to return entities that have since been removed or that have stale data.

It’s worth reiterating that this is not a problem with the persistence provider. Caching is a good thing and the reason why JPA solutions often significantly outperform direct JDBC access in read-mostly applications. The Reference Implementation, for example, uses a sophisticated shared-cache mechanism that is scoped to the entire persistence unit. When operations are completed in a particular persistence context, the results are merged back into the shared cache so that they can be used by other persistence contexts. This happens whether the entity manager and persistence context are created in Java SE or Java EE. Therefore, you can’t assume that closing an entity manager clears test data from the cache.

There are several approaches we can use to keep the cache consistent with our test database. The first, and easiest, is to create and remove test data using the entity manager. Any entity persisted or removed using the entity manager will always be kept consistent with the cache. For small data sets, this is very easy to accomplish. This is the approach we used in Listing 14-9.

For larger data sets, however, it can be cumbersome to create and manage test data using entities. JUnit extensions such as DbUnit3 allow seed data to be defined in XML files and then loaded in bulk to the database before each test begins. So given that the persistence provider won’t know about this data, how can we still make use of it? The first strategy is to establish a set of test data that is read-only. As long as the data is never changed, it doesn’t matter whether the entity exists in the provider cache or not. The second strategy is to either use special data sets for operations that need to modify test data without creating it or to ensure that these changes are never permanently committed. If the transaction to update the database is rolled back, the database and cache state will both remain consistent.

The last thing to consider is explicit cache invalidation. Prior to JPA 2.0, access to the second-level cache was vendor-specific. As discussed in Chapter 11, we can now use the Cache interface to explicitly clear the second-level cache between tests. The following method demonstrates how to invalidate the entire second-level cache given any EntityManagerFactory instance:

public static void clearCache(EntityManagerFactory emf) { emf.getCache().evictAll(); }

If there are any open entity managers, the clear() operation on each should be invoked as well. As we have discussed before, the persistence context is a localized set of transactional changes. It uses data from the shared cache but is actually a separate and distinct data structure.

Switching Configurations for Testing One of the great advantages of JPA is that metadata specified in annotation form may be overridden or replaced by metadata specified in XML form. This affords us a unique opportunity to develop an application targeting the production database platform and then provide an alternate set of mappings (even query definitions) targeted to a test environment. While this is a common practice and has its benefits, it’s worth noting that if you are running on a test database with alternate mappings and query definitions, there will clearly be at least some differences between the test installation and running in production. Production testing is always going to be necessary, but testing earlier in the cycle on a test database can be done on a more convenient or more accessible database platform and can catch some bugs earlier in the cycle.

3 Visit http://dbunit.sourceforge.net/ for more information.

Download at WoweBook.Com

CHAPTER 4 ■ TESTING

443

In the context of testing, the Java SE bootstrap mechanism will use the persistence.xml file located in the META-INF directory on the classpath. As long as the persistence unit definition inside this file has the same name as the one the application was written to, the test version can retarget it as necessary to suit the needs of the integration test.

There are two main uses for this approach. The first is to specify properties in the persistence.xml file that are specific to testing. For many developers, this will mean providing JDBC connection information to a local database so that tests do not collide with other developers on a shared database.

The second major use of a custom persistence.xml file is to customize the database mappings for deployment on a completely different database platform. For example, if Oracle is your production database and you don’t want to run the full database4 on your local machine, you can adjust the mapping information to target an embedded database such as Apache Derby.

As an example of when this would be necessary, consider an application that uses the native sequencing of the Oracle database. Derby does not have an equivalent, so table generators must be used instead. First, let’s consider an example entity that uses a native sequence generator:

@Entity public class Phone { @SequenceGenerator(name="Phone_Gen", sequenceName="PHONE_SEQ") @Id @GeneratedValue(generator="Phone_Gen") private int id; // ... }

The first step to get this entity working on Derby is to create an XML mapping file that overrides the definition of the “Phone_Gen” generator to use a table generator. The following fragment of a mapping file demonstrates how to replace the sequence generator with a table generator:

<entity-mappings> ... <table-generator name="Phone_Gen", table="ID_GEN", pk-column-value="PhoneId"> ... </entity-mappings>

This is the same technique we applied in Chapter 12 when we discussed overriding a sequence generator.

Finally, we need to create a new persistence.xml file that references this mapping file. If the overrides were placed in a mapping file called derby-overrides.xml, the following persistence unit configuration would apply the mapping overrides:

<persistence> <persistence-unit name="hr"> ... <mapping-file>derby-overrides.xml</mapping-file> ... </persistence-unit> </persistence>

4 At the risk of sounding somewhat biased, might we humbly suggest Oracle XE. It represents the power of the Oracle database conveniently sized to an individual machine at no cost. All the examples in this book (including the advanced SQL query examples) were developed on Oracle XE.

Download at WoweBook.Com

CHAPTER 14 ■ TESTING

444

Unlike the mapping file, which sparsely defines overrides, all the information that was present in the production persistence.xml file must be copied into the test-specific version. The only exception to this is the JDBC connection properties, which will now have to be customized for the embedded Derby instance.

Minimizing Database Connections Integration tests execute slower than unit tests due to the nature of the database interaction, but what might not be obvious from the test case shown in Listing 14-9 is that two separate connections are made to the database, one each for the testAuthenticateValidUser() and testAuthenticateInvalidUser() tests. JUnit actually instantiates a new instance of the test case class each time it runs a test method, running setUp() and tearDown() each time as well. The reason for this behavior is to minimize the chance of data stored in fields from one test case interfering with the execution of another.

While this works well for unit tests, it may lead to unacceptable performance for integration tests. To work around this limitation, the @BeforeClass and @AfterClass features of JUnit 4 may be used to create fixtures that run only once for all of the tests in a class. Listing 14-10 demonstrates a test suite class that uses this feature at the level of the entire test suite.

Listing 14-10. One-time Database Setup for Integration Tests

@RunWith(Suite.class) @Suite.SuiteClasses({UserServiceTest3.class}) public class DatabaseTest { public static EntityManagerFactory emf; @BeforeClass public static void setUpBeforeClass() throws Exception { emf = Persistence.createEntityManagerFactory("hr"); } @AfterClass public static void tearDownAfterClass() throws Exception { if (emf != null) { emf.close(); } }}

Using this test suite as a starting point, all test cases added to the @Suite.SuiteClasses annotation can have access to the correctly populated EntityManagerFactory static field on the DatabaseTest class. The setUp() method of each test case now only needs to reference this class to obtain the factory instead of creating it each time. The following example demonstrates the change required for the UnitServiceTest3 test case:

@Before public void setUp() { emf = DatabaseTest.emf; em = emf.createEntityManager(); createTestData(); }

This is a useful technique to minimize the cost of acquiring expensive resources, but care must be taken to ensure that side effects from one test do not accidentally interfere with the execution of other tests. Because all tests share the same entity manager factory, data may be cached or settings may be changed (supported by some entity manager factories) that have an unexpected impact later on. Just as it is necessary to keep the database tables clean between tests, any changes to the entity manager factory

Download at WoweBook.Com

CHAPTER 4 ■ TESTING

445

must be reverted when the test ends, regardless of whether the outcome is a success or a failure. It is usually a good idea to clear the cache, as we showed in the “Test Setup and Teardown” section.

Components and Persistence More often than not, session beans in an integration test are no different from session beans in a unit test. You instantiate the bean, supply any necessary dependencies, and execute the test. Where we start to diverge is when we take into account issues such as transaction management and multiple session bean instances collaborating together to implement a single use case. In the following sections, we will discuss techniques to handle more complex session bean scenarios when testing outside of the container.

Transaction Management Transactions lie at the heart of every enterprise application. We made this statement back in Chapter 3 and drove it home in Chapter 6, demonstrating all the different ways in which entity managers and persistence contexts can intersect with different transaction models. It might come as a surprise, then, to learn that when it comes to writing integration tests, we can often sidestep the stringent transactional requirements of the application to easily develop tests outside the container. The following sections will delve into when transactions are really required and how to translate the container-managed and bean- managed transaction models of the Java EE server into your test environment.

When to Use Transactions

Except for resource-local, application-managed entity managers, which are rarely used in the Java EE environment, transaction management is the purview of session beans and other components that use JPA. We will focus specifically on session beans, but the topics we cover apply equally to transactional persistence operations hosted by transaction-capable components such as message-driven beans or servlets.

The transaction demarcation for a session bean method needs to be considered carefully when writing tests. Despite the default assumption that transactions are used everywhere in the application server, only a select number of methods actually require transaction management for the purpose of testing. Because we are focused on testing persistence, the situation we are concerned with is when the entity manager is being used to persist, merge, or remove entity instances. We also need to determine whether these entities actually need to be persisted to the database.

In a test environment, we are using resource-local, application-managed entity managers. Recall from Chapter 6 that an application-managed entity manager can perform all its operations without an active transaction. In effect, invoking persist() queues up the entity to be persisted the next time a transaction starts and is committed. Furthermore, we know that once an entity is managed, it can typically be located using the find() operation without the need to go to the database. Given these facts, we generally need a transacted entity manager only if the business method creates or modifies entities, and executes a query that should include the results.

Although not required to satisfy business logic, a transaction may also be required if you want the results of the operation to be persisted so that they can be analyzed using something other than the active entity manager. For example, the results of the operation can be read from the database using JDBC and compared to a known value using a test tool.

The main thing we want to stress here before we look into how to implement transactions for session bean tests is that more often than not, you don’t really need them at all. Look at the sequence of operations you are testing and consider whether the outcome will be affected one way or the other first if the data must be written to the database, and later if it truly must be committed as part of the test. Given

Download at WoweBook.Com

CHAPTER 14 ■ TESTING

446

the complexity that manual transaction management can sometimes require, use transactions only when they are necessary.

Container-Managed Transactions

One of the most important benefits of container-managed transactions is that they are configured for session bean methods entirely using metadata. There is no programming interface invoked by the session bean to control the transaction other than the setRollbackOnly() method on the EJBContext interface, and even this occurs only in certain circumstances. Therefore, once we decide that a particular bean method requires a transaction to be active, we need only start a transaction at the start of the test and commit or roll back the results when the test ends.

Listing 14-11 shows a bean method that will require an open transaction during a test. The assignEmployeeToDepartment() method assigns an employee to a given department and then returns the list of employees currently assigned to the department by executing a query. Because the data modification and query occur in the same transaction, our test case will also require a transaction.

Listing 14-11. Business Method Requiring a Transaction

@Stateless public class DepartmentServiceBean implements DepartmentService { private static final String QUERY = "SELECT e " + "FROM Employee e " + "WHERE e.department = ?1 ORDER BY e.name"; @PersistenceContext EntityManager em; public List assignEmployeeToDepartment(int deptId, int empId) { Department dept = em.find(Department.class, deptId); Employee emp = em.find(Employee.class, empId); dept.getEmployees().add(emp); emp.setDepartment(dept); return em.createQuery(QUERY) .setParameter(1, dept) .getResultList(); } // ... }

Because we are using a resource-local entity manager, we will be simulating container-managed transactions with EntityTransaction transactions managed by the test case. Listing 14-12 shows the test case for the assignEmployeeToDepartment() method. We have followed the same template as in Listing 14-9, so the setUp() and tearDown() methods are not shown. Before the session bean method is invoked, we create a new transaction. When the test is complete, we roll back the changes because it isn’t necessary to persist them in the database.

Download at WoweBook.Com

CHAPTER 4 ■ TESTING

447

Listing 14-12. Testing a Business Method that Requires a Transaction

public class DepartmentServiceBeanTest { // ... private void createTestData() { Employee emp = new Employee(500, "Scott"); em.persist(emp); emp = new Employee(600, "John"); em.persist(emp); Department dept = new Department(700, "TEST"); dept.getEmployees().add(emp); emp.setDepartment(dept); em.persist(dept); } @Test public void testAssignEmployeeToDepartment() throws Exception { DepartmentServiceBean bean = new DepartmentServiceBean(); bean.em = em; em.getTransaction().begin(); List result = bean.assignEmployeeToDepartment(700, 500); em.getTransaction().rollback(); Assert.assertEquals(2, result.size()); Assert.assertEquals("John", ((Employee)result.get(0)).getName()); Assert.assertEquals("Scott", ((Employee)result.get(1)).getName()); } // ... }

Bean-Managed Transactions

For a session bean that uses bean-managed transactions, the key issue we need to contend with is the UserTransaction interface. It may or may not be present in any given bean method and can be used for a number of purposes, from checking the transaction status to marking the current transaction for rollback, to committing and rolling back transactions. Fortunately, almost all the UserTransaction methods have a direct correlation to one of the EntityTransaction methods. Because our test strategy involves a single entity manager instance for a test, we need to adapt its EntityTransaction implementation to the UserTransaction interface.

Listing 14-13 shows an implementation of the UserTransaction interface that delegates to the EntityTransaction interface of an EntityManager instance. Exception handling has been added to convert the unchecked exceptions thrown by EntityTransaction operations into the checked exceptions that clients of the UserTransaction interface will be expecting.

Download at WoweBook.Com

nema postavljenih komentara
ovo je samo pregled
3 prikazano na 88 str.