Use case screenshots
config.properties in src/main/resources
app.base.dir=C:/tutorial-trading
Use case git URL
https://github.com/ebundy/jfreechart-bootstrap/tree/master/uc2
UC Analysis
The main user story for this UC
When I run my application, I want to see the stock list names.
The sequence diagram for the main user story
Displaying stock lists should be served by a service class dedicated to list management (list loading, retrieving, updating, etc…) Call it LocalListService class.
The « Local » term insists on the fact that lists handled by this service are on the locale post of the user.
Naming is important. By a simple read, it allows to give a precise meaning and context to our components.
getNamesOfLists() will be the method for retrieving stock lists name. Using the get prefix in the method naming is misleading when we refers models or DTO class and that our method is not a simple getter().
But for a service class, getXXX() should not be confused with a notation JavaBean.
Test and implementation for our UC
Allow to distinct data for tests from data for application
When you use an application, it always more practical when the application developers allow you to choose where the application or data application must be located.
So, we will allow it like we have seen here http://myjavaadventures.com/blog/2015/11/23/creer-son-application-de-trading-en-java-410-tache-identifier-les-services-a-creer-et-ecrire-des-implementations-vides-de-ces-services/
When we deal with unit tests, we have a near enough problematic.
Indeed, data used by a test should not be mixed with data used by the application. In our case, the list data for unit testing should be in a dedicated location. So, we will take advantage of Maven in order to perform it. We will create a new config.properties file in src/test/resources where we define the base application path for our tests.
We can position this path in a directory in the test resources, which seems to be a very suitable place to store test data.
app.base.dir=${basedir}/src/test/resources/tutorial-trading-test |
The path contains the Maven variable ${basedir}. To allow that this variable to be translated in an effective value, we configure our maven pom.xml in order that the Maven process-test-resources phase filter our config.properties in our test resources directory :
<build> <testResources> <testResource> <directory>src/test/resources</directory> <filtering>true</filtering> </testResource> </testResources> </build> |
Wi will use two lists for our tests :
${basedir}/src/test/resources/tutorial-trading-test/liste1.csv
with for content file no data.
ISIN;name;ticker BE0003470755;Solvay;SOLB FR0000045072;Credit Agricole;ACA FR0000073272;Safran;SAF |
${basedir}/src/test/resources/tutorial-trading-test/liste2.csv
with for content file no data.
ISIN;name;ticker US88579Y1010;3M Co.;MMM US0258161092;American Express Co.;AXP US0378331005;Apple Inc.;AAPL US0970231058;Boeing Co.;BA US1491231015;Caterpillar Inc.;CAT US1667641005;Chevron;CVX US17275R1023;Cisco Systems Inc.;CSCO US1912161007;Coca-Cola Co.;KO |
For this use case, files content is useless because it’s not used. We value it when necessary in next use cases.
We will have a configuration class (ApplicationDirs) which will enable our application to work in any environnement (normal running like test environnement).
In normal running, the config.properties in src/main/resources path will be used.
In test running, the config.properties in src/test/resources path will be used.
So, validate it by a unit test that it’s working in test environnement. If it’s ok in test environnement, it will be ok in normal running if config.properties is present at the proper place.
The Failled cases (file missing or property missing) are hard to test. It forces to use tricks to change the priority resource loading for the config.properties. So, we don’t handle them in this unit test.
ApplicationDirsTest.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | package davidhxxx.teach.jfreecharttuto.util; import org.junit.Test; import junit.framework.Assert; public class ApplicationDirsTest { @Test public void static_bloc_configuration_is_ok() throws Exception { String appBaseDir = ApplicationDirs.BASE_DIR; String expectedSubPath = "/src/test/resources/tutorial-trading-test"; int lengthPath = expectedSubPath.length(); String actualSubPath = appBaseDir.substring(appBaseDir.length() - lengthPath); Assert.assertEquals(expectedSubPath, actualSubPath); } } |
ApplicationDirs.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | package davidhxxx.teach.jfreecharttuto.util; import java.io.File; import org.apache.commons.configuration.Configuration; import org.apache.commons.configuration.ConfigurationException; import org.apache.commons.configuration.PropertiesConfiguration; import org.apache.commons.lang.StringUtils; public class ApplicationDirs { static { Configuration config = null; try { config = new PropertiesConfiguration("config.properties"); } catch (ConfigurationException e) { throw new IllegalArgumentException("Erreur lors du chargement du fichier de configuration"); } String appBaseDir = config.getString("app.base.dir"); if (StringUtils.isEmpty(appBaseDir)) { throw new IllegalArgumentException("la propriété doit être renseignée dans le fichier config.properties"); } File repertoireBase = new File(appBaseDir); if (!repertoireBase.exists()) { throw new IllegalArgumentException("le repértoire " + repertoireBase + " n'existe pas"); } if (!repertoireBase.isDirectory()) { throw new IllegalArgumentException("le chemin " + repertoireBase + " n'est pas un répertoire"); } BASE_DIR = appBaseDir; } public static final String BASE_DIR; } |
Remarks on the code :
– We rely on the ApplicationDirs.BASE_DIR constant to know where searching the application data base dir.
Behind the scenes, ApplicationDirs.BASE_DIR is valued from our property app.base.dir in config.properties as soon as the ApplicationDirs class is loaded by the classloader.
– Static bloc allows to initialize once our data base directory.
– Data base directory validity checks are importants because all application data and handling associated will rely on it.
We could go further by checking more things (list directory exists, quotation dir exists, etc…).
We will now write the unit test of the business method.
Build our service with a test acceptance approach
Our acceptance test is simple : I want getNamesOfList of the LocalListService class returns a List with all existing lists in my application. We have defined two of them in src/test/resources… : liste1 and liste2. So, I expect to have them.
LocalListServiceTest.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | package davidhxxx.teach.jfreecharttuto.dataservice; import java.util.Arrays; import java.util.List; import org.junit.Test; import org.unitils.reflectionassert.ReflectionAssert; import org.unitils.reflectionassert.ReflectionComparatorMode; public class LocalListServiceTest { @Test public void getNamesOfLists_returns_list_ordered_by_name() throws Exception { List namesOfLists = LocalListService.getInstance().getNamesOfLists(); List expectedNamesOfLists = Arrays.asList("liste1", "liste2"); ReflectionAssert.assertReflectionEquals(expectedNamesOfLists, namesOfLists, ReflectionComparatorMode.LENIENT_ORDER); } } |
Three remarks on the test:
– Our service class should be a singleton.
Why ?
Because it has no sense to be instantiated multiples times by the application since it ‘s a kind of data referential.
Besides, instantiating it multiples times would cost memory for nothing.
At last, multiple instances would prevent the service to be consistent if later, the service provides methods for list modification. Which is very likely in a full trading application.
The projet doesn’t use Spring (a overload with no advantage in our bootstrap application). So, we will code our singleton in the old-fashion way : a getInstance() static method. That’s why our test method calls getInstance() before calling getNamesOfLists().
– Asserting a list with a classic JUnit assertEquals() is depending on elements order . The Unitils Testing API allows to go beyond from this limitation. ReflectionAssert.assertReflectionEquals(x,x) and the ReflectionComparatorMode.LENIENT_ORDER comparator do the job.
We could begin the implementation of the getNamesOfLists() method.
LocalListService.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | package davidhxxx.teach.jfreecharttuto.dataservice; import java.io.File; import java.io.FilenameFilter; import java.util.List; import davidhxxx.teach.jfreecharttuto.model.ListOfStockList; import davidhxxx.teach.jfreecharttuto.util.ApplicationDirs; public class LocalListService { private static LocalListService instance; private File listsPath; private ListOfStockList listOfStockList; private LocalListService() { setListsPath(new File(ApplicationDirs.BASE_DIR + "/list")); loadListOfStockListNames(); } private void setListsPath(File path) { this.listsPath = path; listsPath.mkdirs(); } public static LocalListService getInstance() { if (instance == null) instance = new LocalListService(); return instance; } public List getNamesOfLists() { return listOfStockList.getNames(); } private void loadListOfStockListNames() { listOfStockList = new ListOfStockList(); File[] files = listsPath.listFiles(new FilenameFilter() { @Override public boolean accept(File dir, String name) { if (name.contains(".csv")) return true; return false; } }); listOfStockList.addListNames(files); } } |
Remarks on the code :
– We use the Bill Pugh singleton implementation. It’s excellent. More details in this article on singleton creation.
– All data needed (list names) are loaded once in the application life since it’s executed in the private constructor of our singleton class.
– for the moment, we don’t care for content of files with csv extensions in the expected directory (list). If it exists, we use its file simple name to guess the list name.
Using our model classes to store and retrieve data
Data initialization for lists will rely on our model classes ListOfStockList and ListOfStock which will be responsible for data store and retrieval .
Our service class LocalListService collaborates with a ListOfStockList instance in order to store list names during the stock lists discovery and init :
listOfStockList.addListNames(files) |
In a second time, the LocalListService class collaborates again with ListOfStockList to retrieve all list names :
public List getNamesOfLists() { return listOfStockList.getNames(); } |
In the same way that we did for LocalListService implementation, we will have a TDD approach.
Our acceptance test is simple : I want addListNames of the ListOfStockList class uses the Files array param to guess the list names (filename minus the file extension) to create as many as ListOfStock instances as needed with no stocks but a list name.
And to check my requirement is fulfilled, I will assert that listOfStockList.getNames() return a List of String with the expected list names.
ListOfStockListTest
package davidhxxx.teach.jfreecharttuto.model; import java.io.File; import java.util.Arrays; import java.util.List; import org.junit.Test; import org.unitils.reflectionassert.ReflectionAssert; import org.unitils.reflectionassert.ReflectionComparatorMode; public class ListOfStockListTest { /* * tested: * - addListNames() * - getNames() * - isStocksLoaded() -> false case */ @Test public void addListNames_add_files_less_extension_without_adding_stocks_AND_getNames_return_them() throws Exception { ListOfStockList listOfStockList = new ListOfStockList(); File[] fileNamesWithExtension = new File[2]; fileNamesWithExtension[0] = new File("listFake1.csv"); fileNamesWithExtension[1] = new File("listFake2.csv"); // action listOfStockList.addListNames(fileNamesWithExtension); // assertion List<String> actualNames = listOfStockList.getNames(); List<String> expectedNames = Arrays.asList("listFake1", "listFake2"); ReflectionAssert.assertReflectionEquals(expectedNames, actualNames, ReflectionComparatorMode.LENIENT_ORDER); } } |
ListOfStockList
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | package davidhxxx.teach.jfreecharttuto.model; import java.io.File; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class ListOfStockList { private Map<String, ListOfStock> listOfStockByListName = new HashMap<String, ListOfStock>(); public void addListNames(File[] filenamesWithExtention) { for (File filenameWithExtention : filenamesWithExtention) { String fileNameWithoutExtension = filenameWithExtention.getName().replace(".csv", ""); listOfStockByListName.put(fileNameWithoutExtension, new ListOfStock()); } } public List<String> getNames() { return new ArrayList<>(listOfStockByListName.keySet()); } } |
Remarks on the code :
– ListOfStockList should be in theory a singleton because there is once ListOfStockList in the application life. But it’s not a very serious problem in this case because LocalListService instantiates once ListOfStockList (in its constructor) .
– Storing the StockList in an inner map (name-
1 2 3 4 5 6 7 8 9 10 | package davidhxxx.teach.jfreecharttuto.model; import java.util.Map; public class ListOfStock { private List<Stock> stocks; } |
Remarks on the code :
– This class has not been updated from our model class diagram because we don’t need more for our use case.
Graphical components
We have build a simple JFrame with only data needed for our use case.
MainFrame.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | package davidhxxx.teach.jfreecharttuto.ui.layout; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.Font; import java.awt.Frame; import java.util.List; import java.util.Locale; import javax.swing.JComboBox; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JScrollPane; import javax.swing.JSplitPane; import javax.swing.JToolBar; import javax.swing.JToolBar.Separator; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; import javax.swing.UIManager; import davidhxxx.teach.jfreecharttuto.dataservice.LocalListService; import davidhxxx.teach.jfreecharttuto.util.WidgetUtil; @SuppressWarnings("serial") public class MainFrame extends JFrame { // buttons private JComboBox comboListOfStockList; // containers private JSplitPane hSplitPaneGeneral; private JToolBar toolBar; private JScrollPane scrollPaneForReferenceChart; public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { new MainFrame("trading bootstrap app"); } }); } public MainFrame(String name) { super(name); Locale.setDefault(new Locale("fr")); UIManager.put("ToolTip.font", new Font("Serif", Font.BOLD, 18)); setVisible(true); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); hSplitPaneGeneral = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); hSplitPaneGeneral.setDividerLocation(150); add(hSplitPaneGeneral, BorderLayout.CENTER); initToolBar(); hSplitPaneGeneral.setLeftComponent(toolBar); hSplitPaneGeneral.setRightComponent(scrollPaneForReferenceChart); setExtendedState(Frame.MAXIMIZED_BOTH); } private void initToolBar() { toolBar = new JToolBar("Still draggable"); toolBar.setOrientation(SwingConstants.VERTICAL); comboListOfStockList = createComboListOfListOfStock(); toolBar.add(comboListOfStockList); createSperationMoyenne(toolBar); } private void createSperationMoyenne(JToolBar toolBar) { toolBar.add(new Separator(new Dimension(1, 30))); } private JComboBox createComboListOfListOfStock() { final JComboBox comboListOfStockList = new JComboBox() { @Override public Dimension getMaximumSize() { return getPreferredSize(); } }; WidgetUtil.setUniqueSizeAndCenter(comboListOfStockList, 120, 30); List listOfListOfStockName = LocalListService.getInstance().getNamesOfLists(); for (String listOfStockName : listOfListOfStockName) { comboListOfStockList.addItem(listOfStockName); } ((JLabel) comboListOfStockList.getRenderer()).setHorizontalAlignment(SwingConstants.CENTER); return comboListOfStockList; } } |