diff --git a/doc/src/technical/howto-develop-an-use-case-in-navalplan.rst b/doc/src/technical/howto-develop-an-use-case-in-navalplan.rst index 905d3f262..3313e2031 100644 --- a/doc/src/technical/howto-develop-an-use-case-in-navalplan.rst +++ b/doc/src/technical/howto-develop-an-use-case-in-navalplan.rst @@ -175,7 +175,9 @@ The new entity ``StretchesFunctionTemplate`` will have the following properties: * ``name``: A string to identify the template. * ``stretches``: A list of ``StretchTemplate`` a new class that will just have - two attributes: ``durationPercentage`` and ``progressPercentage``. + two attributes: ``durationProportion`` and ``progressProportion``. These + would be two percentages defined as ``BigDecimal`` and one based, i.e., 20% + will be 0.20. ``StretchTemplate`` will be a value object as every ``StretchTemplate`` will belong just to one ``StretchesFunctionTemplate`` and would not be modified out @@ -228,20 +230,20 @@ shown): /** * This class is intended as a Hibernate component. It's formed by two - * components, the duration percentage and the progress percentage. It + * components, the duration proportion and the progress proportion. It * represents the different values of a {@link StretchesFunctionTemplate}. * * @author Manuel Rego Casasnovas */ public class StretchTemplate { - public static StretchTemplate create(BigDecimal durationPercentage, - BigDecimal progressPercentage) { - return new StretchTemplate(durationPercentage, progressPercentage); + public static StretchTemplate create(BigDecimal durationProportion, + BigDecimal progressProportion) { + return new StretchTemplate(durationProportion, progressProportion); } - private BigDecimal durationPercentage = BigDecimal.ZERO; - private BigDecimal progressPercentage = BigDecimal.ZERO; + private BigDecimal durationProportion = BigDecimal.ZERO; + private BigDecimal progressProportion = BigDecimal.ZERO; /** * Default constructor for Hibernate. Do not use! @@ -394,16 +396,16 @@ new one if needed):: - + - - @@ -447,10 +449,10 @@ proper ``db.changelog-XXX.xml`` file:: - + - + @@ -1328,9 +1330,9 @@ to show ``StrechTemplate`` information in the window:: final StretchTemplate stretchTemplate = (StretchTemplate) data; row.appendChild(new Label(toStringPercentage(stretchTemplate - .getDurationPercentage()))); + .getDurationProportion()))); row.appendChild(new Label(toStringPercentage(stretchTemplate - .getProgressPercentage()))); + .getProgressProportion()))); row.appendChild(Util.createRemoveButton(new EventListener() { @Override @@ -1483,12 +1485,288 @@ you are just going to retrieve entities and show name information in the listing then select the other option. -Testing (JUnit) -=============== +Testing +======= + +NavalPlan uses JUnit_ testing framework, as a tool to check application +behaviour based on unit tests. The main classes tested are: entities, models and +DAOs. These tests are executed automatically when project is compiled, thus +allow developers check that their changes do not break other parts of software. + +It is strongly recommended to create test for entities and models, in order to +ensure that business logic is working properly. + +.. ADMONITION:: Test Driven Development + + NavalPlan developers usually follow, although not strictly, TDD_ while + programming use cases. The main idea behind TDD is: + + * First write a test to define a expected feature in a class. At this moment, + this is going to be a failing test. + * After that, modify class to fulfill requirements specified by test. + Producing code to pass the test. + +An example of JUnit test +------------------------ + +For example, in order to test that defined mapping is right you can define a +test for your DAO. Simply create the following file called +``StretchesFunctionTemplateDAOTest.java`` in +``navalplanner-business/src/test/java/org/navalplanner/business/test/planner/daos/``: :: - constraint="no empty:${i18n:_('cannot be null or empty')}" + @Transactional + @RunWith(SpringJUnit4ClassRunner.class) + @ContextConfiguration(locations = { BUSINESS_SPRING_CONFIG_FILE, + BUSINESS_SPRING_CONFIG_TEST_FILE }) + public class StretchesFunctionTemplateDAOTest { + + @Autowired + private IStretchesFunctionTemplateDAO dao; + + private StretchesFunctionTemplate stretchesFunctionTemplate; + + private void givenValidStretchesFunctionTemplate() { + stretchesFunctionTemplate = StretchesFunctionTemplate + .create("stretches-function-template-name-" + + UUID.randomUUID()); + stretchesFunctionTemplate.addStretch(StretchTemplate.create( + new BigDecimal(0.25), new BigDecimal(0.1))); + stretchesFunctionTemplate.addStretch(StretchTemplate.create( + new BigDecimal(0.75), new BigDecimal(0.9))); + } + + @Test + public void afterSavingAStretchesFunctionTemplateItExists() { + givenValidStretchesFunctionTemplate(); + dao.save(stretchesFunctionTemplate); + assertTrue(dao.exists(stretchesFunctionTemplate.getId())); + } + + } + +As you can see you need some Spring annotations to run test inside a Spring +context in order to be able to use ``@Autowired`` for different Spring beans, in +that case the DAO class. + +Methods annotated with ``@Test`` will be the ones executed in order to check +different things with methods like ``assertTrue``. + + +Validation +---------- + +In all applications you usually need to validate different data in several +situations. In order to avoid duplicate validations in different layers, +validation logic should take place in domain model. NavalPlan uses `Hibernate +Validator`_ for this task. + +Basic validations +................. + +Entities should be in charge to validate themselves, which means that some +validations should be done in entities. For example, +``StretchesFunctionTemplate`` needs to have a name, then you will add following +annotation:: + + @NotEmpty(message = "name not specified or empty") + public String getName() { + return name; + } + +Then you could add the following test to check that +``StretchesFunctionTemplate`` without name are not stored in database:: + + @Test(expected = ValidationException.class) + public void tryToSaveStretchesFunctionTemplateWithoutName() { + stretchesFunctionTemplate = StretchesFunctionTemplate.create(""); + dao.save(stretchesFunctionTemplate); + } + +As you can see here it is being checked that a ``ValidationException`` is thrown +when it is trying to store an entity with empty name. + +.. NOTE:: + + The different validation annotations like ``@NotNull``, ``@NotEmpty``, + ``@Valid``, etc. should be in ``getXXX`` methods, instead of variables, in + order to avoid proxies when trying to validate entities, because of lazy + initialization in Hibernate. + +Validating related entities +........................... + +Let's go a bit further and try to also validate ``StretchTemplate`` entity, +which is used by ``StretchesFunctionTemplate``, in order to check that values +for proportions should be between 0 and 1. Then you could think in define the +following unit test:: + + @Test(expected = ValidationException.class) + public void tryToSaveStretchesFunctionTemplateWithoutNullStretchTemplate() { + stretchesFunctionTemplate = StretchesFunctionTemplate + .create("stretches-function-template-name-" + UUID.randomUUID()); + stretchesFunctionTemplate.addStretch(StretchTemplate.create( + BigDecimal.TEN, BigDecimal.TEN)); + dao.save(stretchesFunctionTemplate); + } + +If you run this test now it is going to fail as not exception is going to be +thrown. Then you will add ``@Min`` and ``@Max`` annotations to these attributes +in class ``StretchTemplate``:: + + @Min(value = 0, message = "duration proportion is one based percentage so it " + + "should be greater than or equal to 0") + @Max(value = 1, message = "duration proportion is one based percentage so it " + + "should be less than or equal to 1") + public BigDecimal getDurationProportion() { + return durationProportion; + } + + @Min(value = 0, message = "progress proportion is one based percentage so it " + + "should be greater than or equal to 0") + @Max(value = 1, message = "progress proportion is one based percentage so it " + + "should be less than or equal to 1") + public BigDecimal getProgressProportion() { + return progressProportion; + } + +Anyway, test is going to keep failing and you are not getting any +``ValidationException`` yet. This is because of relations are not automatically +navigated during validation, you need to manually specify ``@Valid`` annotation +in order to also validate depending entities. So, you just need to modify +``StretchesFunctionTemplate`` to add the annotation and then test would be +successfully passed:: + + @Valid + public List getStretches() { + return Collections.unmodifiableList(stretches); + } + +Complex validations +................... + +Sometimes you need more complex validations than simply check if a field is +empty or it has some value, in this case you will have to use ``@AssertTrue`` +annotation. There is a convention in NavalPlan for methods annotated with +``@AssertTrue`` that names should start with ``checkConstraint`` prefix. + +For example, if you want to check that inside a ``StretchesFunctionTemplate`` +different ``StretchTemplate`` are correlative. E.g. if you have a stretch with +duration 20% and progress 50%, next stretch should have a greater or equal +progress; then a new stretch with duration 40% and progress 30% is not valid it +should be at least 50% of progress or a greater value. + +Then if you follow TDD, you could add a new test to check if this issue is being +properly validated:: + + @Test(expected = ValidationException.class) + public void checkStretchesProgressOrder() { + stretchesFunctionTemplate = StretchesFunctionTemplate + .create("stretches-function-template-name-" + UUID.randomUUID()); + stretchesFunctionTemplate.addStretch(StretchTemplate.create( + new BigDecimal(0.20), new BigDecimal(0.50))); + stretchesFunctionTemplate.addStretch(StretchTemplate.create( + new BigDecimal(0.40), new BigDecimal(0.30))); + dao.save(stretchesFunctionTemplate); + } + +In order to implement this behaviour you will add following method in +``StretchesFunctionTemplate`` entity:: + + @AssertTrue(message = "Some stretch has less progress value than the " + + "previous stretch") + public boolean checkConstraintStretchesProgressOrder() { + if (stretches.isEmpty()) { + return true; + } + + sortStretchesByDuration(); + + Iterator iterator = stretches.iterator(); + StretchTemplate previous = iterator.next(); + while (iterator.hasNext()) { + StretchTemplate current = iterator.next(); + if (current.getProgressProportion().compareTo( + previous.getProgressProportion()) <= 0) { + return false; + } + previous = current; + } + + return true; + } + + private void sortStretchesByDuration() { + Collections.sort(stretches, new Comparator() { + @Override + public int compare(StretchTemplate o1, StretchTemplate o2) { + return o1.getDurationProportion().compareTo( + o2.getDurationProportion()); + } + }); + } + +At this moment, when a ``StretchesFunctionTemplate`` entity is stored on +database, this constraint will be checked in order to avoid save wrong data. + +.. WARNING:: + + NavalPlan uses a special approach for validating objects when saving, it is + defined in ``GenericDAOHibernate``:: + + /** + * It's necessary to save and validate later. + * + * Validate may retrieve the entity from DB and put it into the Session, which can eventually lead to + * a NonUniqueObject exception. Save works here to reattach the object as well as saving. + */ + public void save(E entity) throws ValidationException { + getSession().saveOrUpdate(entity); + entity.validate(); + } + + As you can see, before validating the entity NavalPlan saves it and then + checks that all validations run successfully. This could lead to some + "strange" results while developing test. + + For example, if you are testing that a value could not be ``null`` and it is + defined with a ``not-null`` constraint in database mapping. You will add + ``@NotNull`` annotation and create a test expecting a ``ValidationException``. + However, as NavalPlan is not able to store in database the entity (because of + database constraint) you will always get a + ``DataIntegrityViolationException``. + +Interface validations +--------------------- + +Even, when it is already stated that validations have to be done in domain +entities, in order to check business logic in proper layer and avoid possible +issues because of wrong data is stored on database. It is also possible to +replicate some of these validations in view layer, in order to show to users +better error messages and prevent them to send invalid data to server. + +ZK provides an easy way to add constraints to form fields. For example, in +``StretchesFunctionTemplate`` entity name can not be empty so you could add the +following validation on ``_editStretchesFunctionTemplate.zul`` file:: + +