At line 21 changed 3 lines. |
* [2] Create a skeleton JSP using XDoclet |
* [3] Create a new ActionTest to test our Action |
* [4] Create a new Action |
* [2] Create skeleton JSPs using XDoclet |
* [3] Create PersonActionTest to test PersonAction |
* [4] Create PersonAction |
At line 80 changed 1 line. |
To create a StrutsTestCase Test for our Action, start by creating a PersonActionTest.java file in the test/web/**/action directory. |
To create a StrutsTestCase Test for PersonAction, start by creating a PersonActionTest.java file in the test/web/**/action directory. |
At line 96 changed 2 lines. |
public void testExecute() { |
// test execute method |
public void testEdit() throws Exception { |
At line 98 added 1 line. |
addRequestParameter("action", "Edit"); |
At line 101 added 3 lines. |
|
verifyForward("edit"); |
assertTrue(request.getAttribute(Constants.PERSON_KEY) != null); |
At line 103 changed 1 line. |
} |
|
public void testSave() throws Exception { |
setRequestPathInfo("/editPerson"); |
addRequestParameter("action", "Edit"); |
addRequestParameter("id", "1"); |
|
actionPerform(); |
|
PersonForm personForm = |
(PersonForm) request.getAttribute(Constants.PERSON_KEY); |
assertTrue(personForm != null); |
|
setRequestPathInfo("/savePerson"); |
addRequestParameter("action", "Save"); |
|
// update the form from the edit and add it back to the request |
personForm.setLastName("Feltz"); |
request.setAttribute(Constants.PERSON_KEY, personForm); |
|
actionPerform(); |
|
verifyForward("edit"); |
verifyNoActionErrors(); |
} |
|
public void testRemove() throws Exception { |
setRequestPathInfo("/editPerson"); |
addRequestParameter("action", "Delete"); |
addRequestParameter("id", "2"); |
actionPerform(); |
|
verifyForward("mainMenu"); |
verifyNoActionErrors(); |
} |
At line 106 changed 1 line. |
Everything should compile at this point (ant compile) since we're not referring to the PersonAction directly in our test. However, if you try to run __ant test-cactus -Dtestcase=PersonAction__, it won't work (make sure Tomcat is ''not'' running if you decide to try this). |
You will need to add __PERSON_KEY__ as a variable to the src/dao/**/Constants.java class. The name, "personForm", matches the name given to the form in the struts-config.xml file. |
At line 108 changed 1 line. |
!!Create a new Action [#4] |
[{Java2HtmlPlugin |
At line 146 added 10 lines. |
/** |
* The request scope attribute that holds the person form. |
*/ |
public static final String PERSON_KEY = "personForm"; |
}] |
|
If you try to run this test, you will get a number of NoSuchMethodErrors - so let's define the edit, save, and delete methods in the PersonAction class. |
|
!!Create PersonAction [#4] |
|
At line 130 changed 4 lines. |
public ActionForward execute(ActionMapping mapping, ActionForm form, |
HttpServletRequest request, |
HttpServletResponse response) |
throws Exception { |
public ActionForward cancel(ActionMapping mapping, ActionForm form, |
HttpServletRequest request, |
HttpServletResponse response) |
throws Exception { |
return mapping.findForward("mainMenu"); |
} |
|
public ActionForward delete(ActionMapping mapping, ActionForm form, |
HttpServletRequest request, |
HttpServletResponse response) |
throws Exception { |
At line 135 changed 1 line. |
log.debug("Entering 'execute' method"); |
log.debug("Entering 'delete' method"); |
At line 138 changed 2 lines. |
// return nothing (yet) |
return null; |
ActionMessages messages = new ActionMessages(); |
PersonForm personForm = (PersonForm) form; |
|
// Exceptions are caught by ActionExceptionHandler |
PersonManager mgr = (PersonManager) getBean("personManager"); |
mgr.removePerson(personForm.getId()); |
|
messages.add(ActionMessages.GLOBAL_MESSAGE, |
new ActionMessage("person.deleted", |
personForm.getFirstName() + " " + |
personForm.getLastName())); |
|
saveMessages(request, messages); |
|
return mapping.findForward("mainMenu"); |
At line 207 added 61 lines. |
|
public ActionForward edit(ActionMapping mapping, ActionForm form, |
HttpServletRequest request, |
HttpServletResponse response) |
throws Exception { |
if (log.isDebugEnabled()) { |
log.debug("Entering 'edit' method"); |
} |
|
PersonForm personForm = (PersonForm) form; |
|
// if an id is passed in, look up the user - otherwise |
// don't do anything - user is doing an add |
if (personForm.getId() != null) { |
PersonManager mgr = (PersonManager) getBean("personManager"); |
Person person = mgr.getPerson(personForm.getId()); |
request.setAttribute(Constants.PERSON_KEY, convert(person)); |
} |
|
return mapping.findForward("edit"); |
} |
|
public ActionForward save(ActionMapping mapping, ActionForm form, |
HttpServletRequest request, |
HttpServletResponse response) |
throws Exception { |
if (log.isDebugEnabled()) { |
log.debug("Entering 'save' method"); |
} |
|
// Extract attributes and parameters we will need |
ActionMessages messages = new ActionMessages(); |
PersonForm personForm = (PersonForm) form; |
boolean isNew = ("".equals(personForm.getId())); |
|
if (log.isDebugEnabled()) { |
log.debug("saving person: " + personForm); |
} |
|
PersonManager mgr = (PersonManager) getBean("personManager"); |
mgr.savePerson(convert(personForm)); |
|
// add success messages |
if (isNew) { |
messages.add(ActionMessages.GLOBAL_MESSAGE, |
new ActionMessage("person.added", |
personForm.getFirstName() + " " + |
personForm.getLastName())); |
request.getSession().setAttribute(Globals.MESSAGE_KEY, messages); |
|
return mapping.findForward("mainMenu"); |
} else { |
messages.add(ActionMessages.GLOBAL_MESSAGE, |
new ActionMessage("person.updated", |
personForm.getFirstName() + " " + |
personForm.getLastName())); |
saveMessages(request, messages); |
|
return mapping.findForward("edit"); |
} |
} |
At line 144 changed 1 line. |
We're not putting much in PersonAction at this point because we just want to 1) render the JSP and 2) verify our Test runs. The XDoclet tags (beginning with ''@struts.action'') will generate the following XML in the build/appfuse/WEB-INF/struts-config.xml file (when you run __ant webdoclet__): |
You'll notice in the code above that there are many calls to to ''convert'' a PersonForm or a Person object. The ''convert'' method is in BaseAction.java (which calls ConvertUtil.convert()) and |
uses |
[BeanUtils.copyProperties|http://jakarta.apache.org/commons/beanutils/api/org/apache/commons/beanutils/BeanUtils.html#copyProperties(java.lang.Object,%20java.lang.Object)] |
to convert POJOs → ActionForms and ActionForms → POJOs. |
At line 276 added 6 lines. |
;:''If you are running Eclipse, you might have to "refresh" the project in order to see PersonForm. It lives in build/web/gen, which should be one of your project's source folders. This is the only way for Eclipse to see and import PersonForm, since it is generated by XDoclet and does not live in your regular source tree. If you are not running Eclipse, you will need to manually add "import org.appfuse.webapp.form.PersonForm;" to PersonAction.java and PersonActionTest.java for the generated PersonForm class to be resolved. You will find it in build/web/gen/org/appfuse/webapp/form/PersonForm.java.'' |
|
;:''In [BaseAction|http://raibledesigns.com/downloads/apptracker/api/org/appfuse/webapp/action/BaseAction.java.html] you can register additional Converters (i.e. [DateConverter|http://raibledesigns.com/downloads/apptracker/api/org/apptracker/util/DateConverter.java.html]) so that BeanUtils.copyProperties knows how to convert Strings → Objects. If you have Lists on your POJOs (i.e. for parent-child relationships), you will need to manually convert those using the {{convertLists(java.lang.Object)}} method.'' |
|
Now we need to add the ''edit'' forward and the ''savePerson'' action-mapping, both with are specified in in our PersonActionTest. To do this, we'll add a couple more XDoclet tags to the top of the PersonAction.java file. Do this right above the class declaration. You should already have the XDoclet tag for the ''editPerson'' action-mapping, but I'm showing it here so you can see all the XDoclet tags at the top of this class. |
|
At line 148 changed 4 lines. |
<action path="/editPerson" type="org.appfuse.webapp.action.PersonAction" |
name="personForm" scope="request" input="mainMenu" |
parameter="action" unknown="false" validate="false"> |
</action> |
/** |
* @struts.action name="personForm" path="/editPerson" scope="request" |
* validate="false" parameter="action" input="mainMenu" |
* @struts.action name="personForm" path="/savePerson" scope="request" |
* validate="true" parameter="action" input="edit" |
* |
* @struts.action-forward name="edit" path=".personDetail" |
*/ |
public final class PersonAction extends BaseAction { |
At line 154 changed 1 line. |
;:''I formatted the XML above the the purposes of the tutorial. No content has changed.'' |
The main difference between the ''editPerson'' and ''savePerson'' action-mappings is that ''savePerson'' has validation turned on (see validation="true") in the XDoclet tag above. Note that the "input" attribute must refer to a forward, and cannot be a path (i.e. /editPerson.html). If you'd prefer to use the save path for both edit and save, that's possible too. Just make sure validate="false", and then in your "save" method - you'll need to call form.validate() and handle errors appropriately. |
At line 297 added 13 lines. |
There are a few keys (ActionMessages) that we need to add to ApplicationResources_en.properties to display the success messages. This file is located in ''web/WEB-INF/classes'' - open it and add the following: |
|
;:''I usually add these under the {{# -- success messages --}} comment.'' |
|
{{{person.added=Information for <strong>{0}</strong> has been added successfully. |
person.deleted=Information for <strong>{0}</strong> has been deleted successfully. |
person.updated=Information for <strong>{0}</strong> has been updated successfully. |
}}} |
|
You could use generic ''added'', ''deleted'' and ''updated'' messages, whatever works for you. It's nice to have separate messages in case these need to change on a per-entity basis. |
|
You might notice that the code we're using to call the PersonManager is the same as the code we used in our PersonManagerTest. Both PersonAction and PersonManagerTest are ''clients'' of PersonManagerImpl, so this makes perfect sense. |
|
At line 158 changed 1 line. |
!!Display the JSP in a browser and run the ActionTest [#5] |
!!Run PersonActionTest [#6] |
At line 160 changed 1 line. |
To test the JSP visually in your browser, save everything, run __ant deploy__, start Tomcat, and navigate to [http://localhost:8080/appfuse/personForm.jsp]. You should see something similar to the following image in your browser: |
If you look at our PersonActionTest, all our tests depend on having a record with id=1 in the database (and testRemove depends on id=2), so let's add that to our sample data file (metadata/sql/sample-data.xml). I'd just add it at the bottom - order is not important since it (currently) does not relate to any other tables. |
At line 162 changed 2 lines. |
%%(border: 1px solid black; height: 125px; width: 270px; margin: 0 auto;) |
[personForm-plain.png] |
{{{ |
<table name='person'> |
<column>id</column> |
<column>first_name</column> |
<column>last_name</column> |
<row> |
<value>1</value> |
<value>Matt</value> |
<value>Raible</value> |
</row> |
<row> |
<value>2</value> |
<value>James</value> |
<value>Davidson</value> |
</row> |
</table> |
}}} |
|
DBUnit loads this file before we run any of our tests, so this record will be available to our Action test. |
|
Now if you run __ant test-cactus -Dtestcase=PersonAction__ - everything should work as planned. Make sure Tomcat isn't running before you try this. |
|
%%(color:green)BUILD SUCCESSFUL\\ |
Total time: 1 minute 21 seconds%% |
|
!!Clean up the JSP to make it presentable [#7] |
First, let's clean up our personForm.jsp by making the "id" property a hidden field. Remove the following code block: |
|
[{Java2HtmlPlugin |
|
<tr> |
<th> |
<appfuse:label key="personForm.id"/> |
</th> |
<td> |
<html:text property="id" styleId="id"/> |
</td> |
</tr> |
}] |
|
And add the following before the <table> tag: |
|
[{Java2HtmlPlugin |
|
<html:hidden property="id"/> |
}] |
|
You should probably also change the ''action'' of the <html:form> to be "savePerson" so validation will be turned on when saving. Also, change the ''focus'' attribute from focus="" to focus="firstName" so the cursor will be in the firstName field when the page loads (this is done with JavaScript). |
|
Now if you execute __ant db-load deploy-web__, start Tomcat and point your browser to [http://localhost:8080/appfuse/editPerson.html?id=1], you should see something like this: |
|
%%(border: 1px solid black; margin: 0 auto; height: 166px; width: 337px) |
[personForm-final.png] |
At line 166 changed 1 line. |
;:''There is also a __deploy-web__ target in build.xml that will allow you to just deploy the files in the web directory. Nothing gets compiled or generated when you use this target. If you'd like, you can [learn more about available ant targets|AppFuseAntTasks].'' |
Finally, to make this page more user friendly, you may want to add a message for your users at the top of the form, but this can easily be done by adding text (using <fmt:message>) at the top of the personForm.jsp page. |
At line 168 changed 1 line. |
Now, if you stop Tomcat and run __ant test-cactus -Dtestcase=PersonAction__, that should work too! |
!![[Optional] Create a Canoo WebTest to test browser-like actions [#7] |
The final (optional) step in this tutorial is to create a [Canoo WebTest|http://webtest.canoo.com] to test the JSPs. |
At line 376 added 100 lines. |
;:''I say this step is optional, because you can run the same tests through your browser.'' |
|
You can use the following URLs to test the different actions for adding, editing and saving a user. |
|
* Add - [http://localhost:8080/appfuse/editPerson.html]. |
* Edit - [http://localhost:8080/appfuse/editPerson.html?id=1] (make sure and run __ant db-load__ first). |
* Delete - [http://localhost:8080/appfuse/editPerson.html?action=Delete&id=1] (or edit and click on the Delete button). |
* Save - Click [edit|http://localhost:8080/appfuse/editPerson.html?id=1] and then click the Save button. |
|
Canoo tests are pretty slick in that they're simply configured in an XML file. To add tests for add, edit, save and delete, open test/web/web-tests.xml and add the following XML. You'll notice that this fragment has a target named ''PersonTests'' that runs all the related tests. |
|
;:''I use CamelCase target names (vs. the traditional lowercase, dash-separated) because when you're typing ''-Dtestcase=Name'', I've found that I'm used to doing CamelCase for my JUnit Tests.'' |
|
[{Java2HtmlPlugin |
|
<!-- runs person-related tests --> |
<target name="PersonTests" |
depends="EditPerson,SavePerson,AddPerson,DeletePerson" |
description="Call and executes all person test cases (targets)"> |
<echo>Successfully ran all Person JSP tests!</echo> |
</target> |
|
<!-- Verify the edit person screen displays without errors --> |
<target name="EditPerson" |
description="Tests editing an existing Person's information"> |
<webtest name="editPerson"> |
&config; |
<steps> |
&login; |
<invoke description="click Edit Person link" url="/editPerson.html?id=1"/> |
<verifytitle description="we should see the personDetail title" |
text=".*${personDetail.title}.*" regex="true"/> |
</steps> |
</webtest> |
</target> |
|
<!-- Edit a person and then save --> |
<target name="SavePerson" |
description="Tests editing and saving a user"> |
<webtest name="savePerson"> |
&config; |
<steps> |
&login; |
<invoke description="click Edit Person link" url="/editPerson.html?id=1"/> |
<verifytitle description="we should see the personDetail title" |
text=".*${personDetail.title}.*" regex="true"/> |
<setinputfield description="set lastName" name="lastName" value="Canoo"/> |
<clickbutton label="Save" description="Click Save"/> |
<verifytitle description="Page re-appears if save successful" |
text=".*${personDetail.title}.*" regex="true"/> |
</steps> |
</webtest> |
</target> |
|
<!-- Add a new Person --> |
<target name="AddPerson" |
description="Adds a new Person"> |
<webtest name="addPerson"> |
&config; |
<steps> |
&login; |
<invoke description="click Add Button" url="/editPerson.html"/> |
<verifytitle description="we should see the personDetail title" |
text=".*${personDetail.title}.*" regex="true"/> |
<setinputfield description="set firstName" name="firstName" value="Abbie"/> |
<setinputfield description="set lastName" name="lastName" value="Raible"/> |
<clickbutton label="${button.save}" description="Click button 'Save'"/> |
<verifytitle description="Main Menu appears if save successful" |
text=".*${mainMenu.title}.*" regex="true"/> |
<verifytext description="verify success message" |
text="Information for <strong>Abbie Raible</strong> has been added successfully."/> |
</steps> |
</webtest> |
</target> |
|
<!-- Delete existing person --> |
<target name="DeletePerson" |
description="Deletes existing Person"> |
<webtest name="deletePerson"> |
&config; |
<steps> |
&login; |
<invoke description="click Edit Person link" url="/editPerson.html?id=1"/> |
<clickbutton label="${button.delete}" description="Click button 'Delete'"/> |
<verifytitle description="display Main Menu" text=".*${mainMenu.title}.*" regex="true"/> |
<verifytext description="verify success message" |
text="Information for <strong>Matt Canoo</strong> has been deleted successfully."/> |
</steps> |
</webtest> |
</target> |
}] |
|
After adding this, you should be able to run __ant test-canoo -Dtestcase=PersonTests__ with Tomcat running or __ant test-jsp -Dtestcase=PersonTests__ if you want Ant to start/stop Tomcat for you. To include the PersonTests when all Canoo tests are run, add it as a dependency to the "run-all-tests" target. |
|
You'll notice that there's no logging in the client-side window by Canoo. If you'd like to see what it's doing, you can add the following between </webtest> and </target> at the end of each target. |
|
{{{<loadfile property="web-tests.result" |
srcFile="${test.dir}/data/web-tests-result.xml"/> |
<echo>${web-tests.result}</echo>}}} |
|
At line 171 changed 1 line. |
Total time: 51 seconds%% |
Total time: 11 seconds%% |
At line 173 removed 1 line. |
;:''Look in your console's log for <span style="color: purple">PersonAction.execute(33) | Entering 'execute' method</span>. This is the log.debug statement we put in our execute method. You should also be able to [view personForm.jsp|http://localhost:8080/appfuse/personForm.jsp] (make sure Tomcat is running and you've logged into AppFuse) and click the "Save" button to see the same debug message. You may have to login after clicking "Save" since all actions are protected.'' |
At line 177 changed 1 line. |
''Next Up:'' __Part IV:__ [Configuring JSP and Action CRUD methods|ConfiguringTiles] - Integrating personForm.jsp with Struts, replacing execute with different CRUD methods (add, edit, delete), customizing the JSP so it looks good and finally - writing a WebTest to test the JSPs functionality. |
''Next Up:'' __Part V:__ [Adding Validation and List Screen|ValidationAndList] - Adding validation logic to the personForm so that firstName and lastName are required fields and adding a list screen to display all person records in the database. |