Running Maven Defaults and Overriding with TestNG

The standard for any team, maintaining a Maven project (or similar for any project), is that it should be downloadable to a users local environment, and the capability to run “mvn clean”, “mvn compile”, “mvn test” within an hour. This includes having to install Java and Maven if necessary. No assumptions on the given environment.

Now, if you want to configure testNG to run with .xml files, you can use the suiteXmlFiles tag in the pom.xml file as discussed in Notes on Using Maven and TestNG, but then you will have to include a TestNG xml file. This is bad. The way you get new developers to download and run your code quickly is by having few steps and no configurations. Defaults should be provided to get them to run your code as quickly as possible. It is okay if the code doesn’t do exactly what it was intended to do. Once they see the main purpose of your code, as a simple user, you can inspire them to learn how to configure to be an advanced user.

The way to get around this issue with TestNG is to give all your tests the TestNG annotation, turn them all to false, except the one you actually want to run by default. Those are set to true.


public class TestNGEntryPoint {
    public static void main(String[] args) {
        System.out.println("main start");
        try {
            String classToRun;
            String methodToRun;
            if (args.length == 2) {
                classToRun = args[0];
                methodToRun = args[1];
            } else {
                classToRun = MyFirstTest.class.getName();
                methodToRun = MyFirstTest.class.getMethod(
					    "testFirstTest", new Class[0]).getName();
            }
            new TestNGEntryPoint(classToRun, methodToRun);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        System.out.println("main finish");
    }
	
    public TestNGEntryPoint(String className, String methodName) {
        // Create Suite List
        List suites = new ArrayList();

        // Add Suite to Suite List
        XmlSuite suite = new XmlSuite();
        suites.add(suite);
        suite.setName("MyTestSuite");

        // Add Test to Suite
        XmlTest test = new XmlTest(suite);
        test.setName("MyTest");

        // Add Class List to Test
        List classes = new ArrayList();
        test.setXmlClasses(classes);

        // Add Class to Class List
        XmlClass clazz = new XmlClass(className);
        classes.add(clazz); 

        // Run TestNG
        TestNG testNG = new TestNG();
        testNG.setXmlSuites(suites);
        testNG.addListener(new TestNGAnnotationTransformer(methodName));
        testNG.run();
    }
	
    public static class TestNGAnnotationTransformer implements IAnnotationTransformer{
        String methodToRun;
        public TestNGAnnotationTransformer(String methodName) {
            methodToRun = methodName;
        }
        public void transform(ITestAnnotation annotation, Class arg1, Constructor arg2, Method testMethod) {
            if (methodToRun.equals(testMethod.getName())) {
                annotation.setEnabled(true);
            }
        }
    }
}

Add a main method in MyFirstTest that looks like the following:


package com.mycompany.myapp;
public class MyFirstTest {
	  
	public static void main (String[] args) {
		new TestNGEntryPoint(MyFirstTest.class.getName(), "testFirstTest");
	}
	 
	
  @Test(enabled=false)
  public void testFirstTest()

You can then run these tests from maven directly using:


mvn exec:java -Dexec.classpathScope="test" -Dexec.mainClass="com.mycompany.myapp.MyFirstTest"

Just as another sort of pet peeve, the way the TestNG object is build should be capable of building top down or bottom up. It is always good to have standard behaviors. For instance, since the line “new XmlTest(suite)” uses the suite to configure several defaults for the XMLTest, this is great. However, you should also be able to do new “XmlSuite(suites)” or new XmlClass(classes). There should be the ability to build bottom up from class to test to suite or to build top down from suite to test to class.

Working with Maven and TestNG

My final recommendation is not to use the xml tag in the pom.xml file to run TestNG with an XML file. Rather, leave “mvn test” running a default set of tests, which will be all classes in your src/test/java directory which end with “Test” and within those tests, all methods annotated with “@Test” annotation.

Then, run additional tests with “mvn test -Dtest=MySecond” or Right Click->Run As>TestNG Test. You can also programmatically build up a test suite to run as shown previously.

Notes on Using Maven and TestNG

This article is written to introduce the concept of a one click build. You should allow downloading and running of any tests or default configuration of tests within minutes. The article is written is such a way that you can include testng in your project, then verify that you can run from an xml file, then verify that you can modify the default annotations to configure whichever run you would like.

Including TestNG in Your Project

Suppose you have included TestNG in your project. That is in the pom.xml file, you have added the dependency and you have installed the jar in your local Maven repository (if you aren’t using Maven, you have included the jar as a dependency in your project).


  <dependency>
    <groupId>org.testng</groupId>
    <artifactId>testng</artifactId>
    <version>6.1.1</version>
  </dependency>

This will allow you to automatically run tests from the command line, via “mvn test”. You simply have to add the testng annotation to the method. Your class should be in the “src/test/java” source folder which Maven uses as defaults for tests.


public class MyFirstTest {
    @Test(enabled=true)
    public void test1() {
        System.out.println("This is a test");
    }
}

The “mvn test” command will automatically run all tests with the Test annotation in your “src/test/java” folder.

Configuring TestNG Through TestNG XML Files

Now, suppose you want to run some specific tests. What you can do is include a TestNG XML configuration file.

In your pom.xml file, setup to


  <build>
...
    <plugins>
...
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>2.4.3</version>
        <configuration>
...
          <suiteXmlFiles>
            <suiteXmlFile>${project.basedir}/${TESTNG_FILE}</suiteXmlFile>
          </suiteXmlFiles>
        </configuration>
      </plugin>
    </plugins>
  </build>

You may then execute a specific testng xml file using “mvn test -DTESTNG_FILE=MyFirstTest.xml”, assuming that the MyFirstTest.xml file exists in the same directory as your pom.xml file, where you are running the mvn command (on a side note, you may use -f in the mvn command to include a path to your pom.xml file).

Your testng file might look like the following:


<!DOCTYPE suite SYSTEM "http://beust.com/testng/testng-1.0.dtd" >
<suite name="MyTestNGSuite" parallel="methods"
	data-provider-thread-count="3" thread-count="3">
  <test name="MyTestNGTest">
    <classes>
      <class name="MyFirstTest">
        <methods>
          <include name="test1" />
        </methods>
      </class>
    </classes>
  </test>
</suite>

Using Annotation Transformers with TestNG XML Files

Now, suppose you had a class which you did not want to run by default using “mvn test”. Then, you could add the annotation “enabled=false”. For example, we might have another class that looks like so:


public class MyDisabledTest implements IAnnotationTransformer {
    public void transform(ITestAnnotation annotation, Class arg1, Constructor arg2, Method testMethod) {
        if ("testDisabled".equals(testMethod.getName())) {
            annotation.setEnabled(true);
        }
    }    

    @Test(enabled=false)
    public void testDisabled() {
        System.out.println("This is a disabled test");
    }
}

By default, when we ran “mvn test” or “mvn test -DTESTNG_FILE=MyFirstTest.xml”, the disabled test would not run. However, we can introduce how to change annotations via the TestNG xml file and run this disabled test using the Annotation Transformer. Add the following file to the same directory where the pom.xml file exists, naming it “MyDisabledTest.xml”.


<!DOCTYPE suite SYSTEM "http://beust.com/testng/testng-1.0.dtd" >
<suite name="MyTestNGSuite" parallel="methods"
	data-provider-thread-count="3" thread-count="3">
  <listeners>
    <listener class-name="MyDisabledTest"/>
  </listeners>
  <test name="MyTestNGTest">
    <classes>
      <class name="MyDisabledTest">
        <methods>
          <include name="testDisabled" />
        </methods>
      </class>
    </classes>
  </test>
</suite>

We can now run the disabled test via “mvn test -DTESTNG_FILE=MyDisabledTest.xml”

Restful Thinking with Your Webpage

Just in general, if you have a site that you are maintaining and have a large user base, you should have the requirement that any view the user ever has is accessible from a given url.

That is, if a popup menu is showing, if an image is on the screen, this should be accessible from a given url that someone can pop into their web browser to see what you are seeing.

If you have a workflow, which is a series of pages that a user must go through to perform a process, each page in that workflow should be accessible from a given url.

Using Javascript to Show and Hide Div Elements

This article introduces you to javascript and showing and hiding elements. This code was written to allow dynamically adding multiple elements in a list and having show/hide automatically generate the elements without the need to give ids to the elements.


    <html>
    <head>
    <script>
     function myShow(node) {
  node.innerHTML = node.innerHTML + "_SIB1_" + node.nextSibling.nodeName; // Text
  node.innerHTML = node.innerHTML + "_SIB2_" + node.nextSibling.nextSibling.nodeName; // Div
  node.innerHTML = node.innerHTML + "_SIB2_ID_" + node.nextSibling.nextSibling.id; // myDiv
  node.innerHTML = node.innerHTML + "_SIB2_VISIBILITY_" + node.nextSibling.nextSibling.style.visibility;
  node.nextSibling.nextSibling.style.visibility = 'visible';
 }

 function myHide(node) {
  node.innerHTML = node.innerHTML + '_P_' + node.parentNode.nodeName; // Div  node.innerHTML = node.innerHTML + '_P_ID_' + node.parentNode.id; // myDiv
  node.innerHTML = node.innerHTML + "_P_VISIBILITY_" + node.parentNode.style.visibility;
  node.parentNode.style.visibility = 'hidden';
 }
    </script>
    </head>
    <body>
    <a title='myShowLink' onclick='myShow(this)'>Click to Show Div</a>
    <div id='myDiv'>
        <a title='myHideLink' onclick='myHide(this)'>Click to Hide Div</a>
        More Info
    </div>
    </body>
    </html>

The innerHTML lines are just for helping to understand what the code is doing. You may hide them by commenting them out with // or just deleting them.

Anyone Can Run Any Code Any Time

Joel on Software introduced me to the concept of the one click build.

Can a new developer to your team, download your code and do something, anything which verifies that your code actually works? Within an hour.

Can a new consumer, download your project, open, run and use your tool within a day, knowing nothing about nothing?

Are people afraid to change the code, afraid of breaking something or releasing broken code? This means you don’t have the right automations in place and your development will slowly come to a halt. In fact, answering the first two questions in the affirmative also typically means that you have great processes in place. If your code is confusing to run and difficult to use, most likely, you yourself can’t automate the checks necessary that it is good working code. Forcing yourself to do this will also simplify the use of your tool.

Do you have requirements that require people before checking in code? This is a big no, no as this will bottleneck development. Anyone should be able to checkin, anytime, anywhere. Multiple developers should be able to merge code anytime, anywhere. Note that I didn’t say this merged code or checkin code will automatically go into a release, right away.

All processes to review changes and determine what is release worthy should be after committing. The purpose of version control is to release faster. If you don’t allow refactoring of code, your code will soon become so convoluted, that development will halt. If you don’t have the right automations in place and reviews must occur before committing code or integrating code, refactoring and simplifying code simply won’t happen. Or you might have to tell all the developers to stop and not do anything while the one person, who understands the code, does something.

Anyone should be able to commit, anytime, anywhere. Automate the checks that you would do manually and you will be able to release faster.

Having one person on the team who understands what is necessary to release, just won’t cut it. Everyone on your team should be able to release new versions. If every single person on your team can give a release, even if the main person is on vacation, its a sign you might have a one click build.

If a customer asks for a change and you can deliver within an hour, its a sign you have a one click build. Deliver, with confidence, that is.

Creating Invertible Functions

In the last post, I wrote some functions about using Java’s color functions. If you notice, there are a lot of functions which undo the other functions. I believe each class that passes along data and converts data, should be able to convert in the opposite direction.

Functions should also do the opposite of what others do.


public void write(File outputFile, String data)
public String read(File inputFile)
public Color getColorObject(int[] rgb)
public int[] getColorValues(Color color)

You may have convenience functions to take in 3 arguments instead of an int[] but the underlying meat function should take in what the associate function puts out.

Also, rather that passing in a String which represents an input path, you should use an object, like a File object. Doing so, will validate the data passed into the conversion function automatically.

Working with Colors in Java

It is often good to leave configurations like standard colors or strings in a configuration class, so that you can use standard colors throughout your code. For instance, it is common to use green to mean good, red to mean bad and yellow to mean warning.

In Java, you can create colors directly from RGB, HSB or HexStrings:


package com.mycompany.app.util;

import java.awt.Color;

public class ColorHelper {
	public static void main(String[] args) {
		Color red1 = getRGBColor(128,0,0);
		Color red2 = getHSBColor(0,240,60);
		Color red3 = getHexStringColor("#800000");
		Color green1 = getRGBColor(0,128,0);
		Color green2 = getHSBColor(80,240,60);
		Color green3 = getHexStringColor("#008000");
		Color yellow1 = getRGBColor(128,128,0);
		Color yellow2 = getHSBColor(40,240,60);
		Color yellow3 = getHexStringColor("#808000");
		System.out.println("RED:"+getHexStringValue(red1));
		System.out.println("RED2:"+getHexStringValue(red2));
		System.out.println("RED:"+getHexStringValue(red3));
		System.out.println("GREEN:"+getHexStringValue(green1));
		System.out.println("GREEN:"+getHexStringValue(green2));
		System.out.println("GREEN:"+getHexStringValue(green3));
		System.out.println("YELLOW:"+getHexStringValue(yellow1));
		System.out.println("YELLOW:"+getHexStringValue(yellow2));
		System.out.println("YELLOW:"+getHexStringValue(yellow3));
		
		// Check invertible functions
		Color red4 = getRGBColor(getRGBValues(red1));
		int[] red4RGBValues = getRGBValues(getRGBColor(new int[]{128,0,0}));
		System.out.println("red4:"+getHexStringValue(red4));
		System.out.println("red4RGBValues:"+getHexStringValue(getRGBColor(red4RGBValues)));
		Color red5 = getHSBColor(getHSBValues(red1));
		int[] red5HSBValues = getHSBValues(getHSBColor(new int[]{0,240,60}));
		System.out.println("red5:"+getHexStringValue(red5));
		System.out.println("red5HSBValues:"+getHexStringValue(getHSBColor(red5HSBValues)));
		Color red6 = getHexStringColor(getHexStringValue(red1));
		String red6HexString = getHexStringValue(getHexStringColor("#800000"));
		System.out.println("red6:"+getHexStringValue(red6));
		System.out.println("red6HexString:"+red6HexString);
	}
	// Convenience Functions
	public static Color getRGBColor(int red, int green, int blue) {
		return getRGBColor(new int[]{red, green, blue});
	}
	public static Color getHSBColor(int hue, int sat, int lum) {
		return getHSBColor(new int[]{ hue, sat, lum});
	}
	// Invertible Functions
	public static Color getRGBColor(int[] myRGB) {
		return new Color(myRGB[0], myRGB[1], myRGB[2]);
	}
	public static int[] getRGBValues(Color color) {
		return new int[]{color.getRed(), color.getGreen(), color.getBlue()};
	}
	public static Color getHSBColor(int[] myHSB) {
		float hue = (float) (myHSB[0]/240.0); 
		float sat = (float) (myHSB[1]/240.0);
		float lum = (float) (myHSB[2]/120.0);
		return Color.getHSBColor(hue, sat, lum);
	}
	public static int[] getHSBValues(Color color) {
		float[] myHSB = Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getBlue(), new float[3]);
		return new int[]{ (int) (myHSB[0]*240), (int) (myHSB[1]*240), (int) (myHSB[2]*120)};
	}
	public static Color getHexStringColor(String hexString) {
		return Color.decode(hexString);
	}
	public static String getHexStringValue(Color color) {
		String redHex = get2DigitHexString(color.getRed());
		String greenHex = get2DigitHexString(color.getGreen());
		String blueHex = get2DigitHexString(color.getBlue());
		return "#" + redHex + greenHex + blueHex;
	}
	// Private Functions
	/**
	 * Takes in a value between 0 and 255
	 * @param decimalValue
	 * @return
	 */
	private static String get2DigitHexString(int decimalValue) {
		String hexString = Integer.toHexString(decimalValue);
		return hexString.length() == 1 ? '0' + hexString : hexString;
	}
}

Common Commands when Working on a Mac

Dashboard->Finder>Applications>Utilities>Terminal

Command+X=Cut
Command+C=Copy
Command+V=Paste


java -version
mvn -version
cd /Users/scottizu/Desktop
cd /Users/scottizu/Downloads
/usr/local/git/bin/git clone /Users/scottizu/Desktop/myrepo/myrepo.git

myrepo.git/objects/0d/36ed3e5e3edec44b45675091ed291c1c1fcd87: The requested document cannot be retrieved at this time.
myrepo.git/objects/0e/ddb7cc3b81975db96c4cb5a7c64526dafab660: The requested document cannot be retrieved at this time.
myrepo.git/objects/0f/84678662bf60b776c2c1cc5693a00746f328b9: The requested document cannot be retrieved at this time.
myrepo.git/objects/39/ec569939eed7ec58a8f89e88d75e475ecc1e0e: The requested document cannot be retrieved at this time.

cd /Users/scottizu/Downloads/myrepo/my-app

curl http://www.jfugue.org/jfugue-4.0.3-with-musicxml.jar > jfugue-4.0.3-with-musicxml.jar
/usr/bin/mvn install:install-file -Dfile=jfugue-4.0.3-with-musicxml.jar -Dpackaging=jar -DgroupId=org -DartifactId=jfugue -Dversion=4.0.3

curl http://sourceforge.net/projects/jmathplot/files/latest/download > jmathplot-1.0.jar
/usr/bin/mvn install:install-file -Dfile=jmathplot-1.0.jar -Dpackaging=jar -DgroupId=org -DartifactId=jmathplot -Dversion=1.0

curl http://dl.ghost4j.org/ghost4j-0.5.0.zip > ghost4j-0.5.0.zip
open ghost4j-0.5.0.zip
/usr/bin/mvn install:install-file -Dfile=ghost4j-0.5.0/ghost4j-0.5.0.jar -Dpackaging=jar -DgroupId=org.ghost4j -DartifactId=ghost4j -Dversion=0.5.0

/usr/bin/mvn install:install-file -Dfile=ghost4j-0.5.0/lib/xmlgraphics-commons-1.4.jar -Dpackaging=jar -DgroupId=org.ghost4j -DartifactId=xmlgraphics-commons -Dversion=1.4

/usr/bin/mvn install:install-file -Dfile=ghost4j-0.5.0/lib/commons-io-1.3.1.jar -Dpackaging=jar -DgroupId=org.ghost4j -DartifactId=commons-io -Dversion=1.3.1

/usr/bin/mvn install:install-file -Dfile=ghost4j-0.5.0/lib/log4j-1.2.15.jar -Packaging=jar -DgroupId=org.ghost4j -DartifactId=log4j -Dversion=1.2.15

/usr/bin/mvn install:install-file -Dfile=ghost4j-0.5.0/lib/commons-beanutils-1.8.3.jar -Dpackaging=jar -DgroupId=org.ghost4j -DartifactId=commons-beanutils -Dversion=1.8.3

/usr/bin/mvn install:install-file -Dfile=ghost4j-0.5.0/lib/commons-logging-1.1.1.jar -Dpackaging=jar -DgroupId=org.ghost4j -DartifactId=commons-logging -Dversion=1.1.1

/usr/bin/mvn install:install-file -Dfile=ghost4j-0.5.0/lib/itext-2.1.7.jar -Dpackaging=jar -DgroupId=org.ghost4j -DartifactId=itext -Dversion=2.1.7

/usr/bin/mvn install:install-file -Dfile=ghost4j-0.5.0/lib/jna-3.3.0.jar -Dpackaging=jar -DgroupId=org.ghost4j -DartifactId=jna -Dversion=3.3.0

Tar ball
eclipse-standard-kepler-R-macosx-cocoa-x86_64.tar

Finding the Location of Menus and UI Automation

This code can help you automate finding the location of a JComponent (like JMenu and JMenuItem) to automate clicking them on the screen.


	public static void mouseClickMenu(JFrame frame, String menuLabel,
			String itemLabel) {
		JMenuBar bar = frame.getJMenuBar();
		for(int i=0; i<bar.getMenuCount(); i++) {
			JMenu menu = bar.getMenu(i);
			if(menu.getText().equals(menuLabel)){
				for(int j=0; j<menu.getItemCount(); j++) {
					JMenuItem item = menu.getItem(j);
					if(item.getText().equals(itemLabel)) {
						mouseClickMenu(frame, bar, menu, item);
					}
				}
			}
		}
	}
	
	public static Point getLocationOnFrame(Component component, Point oldPoint) {
		if(component.getParent() == null) { // Don't get offset for top level frame
			return oldPoint;
		} else {
			Point newPoint = new Point((int) component.getX()+(int)oldPoint.getX(), (int) component.getY()+(int) oldPoint.getY());
			return getLocationOnFrame(component.getParent(), newPoint);
		}
	}

	private static void mouseClickMenu(JFrame frame, JMenuBar bar, JMenu menu,
			JMenuItem item) {
		Point menuLoc = getLocationOnFrame(menu, new Point(0,0));
		mouseClick(frame, InputEvent.BUTTON1_MASK, menuLoc, 1);
		Point itemLoc = getLocationOnFrame(item, new Point(0,0));
		mouseClick(frame, InputEvent.BUTTON1_MASK, itemLoc, 1);
	}

The mouse click is a series of mouse pressed, mouse released and mouse clicked as shown in An Introduction to Posting Mouse and Key Events in Java.

Finding the Exact Location of Java Swing Components

For a JFrame, if you use the following code:


JFrame frame = new JFrame();
frame.setLayout(new BorderLayout());
frame.setSize(600, 800);
frame.setLocation(30,50);
frame.setVisible(true);

you will likely not be able to actually use the total 600 width and 800 height. To find the usable space for a JFrame, you will need to look at its content pane.


    	int cpWidth = (int) frame.getContentPane().getWidth();
    	int cpHeight = (int) frame.getContentPane().getHeight();
    	int cpXOffset = (int) frame.getContentPane().getLocationOnScreen().getX() - (int) this.getLocationOnScreen().getX();
    	int cpYOffset = (int) frame.getContentPane().getLocationOnScreen().getY() - (int) this.getLocationOnScreen().getY();

This will give you the usable width, height and the offset from the frames top left point. For a JFrame, you use setSize, while for other JComponents within the JFrame, you use setPreferredSize() since this will allow you to not be accurate with your layout. If you use a JScrollPane, you will also have some trouble finding the adjusted space. In this case, you will want to do:


    panel = new JPanel();
    scrollPane = new JScrollPane(panel);
    ...
    public void adjustToScrollPaneDimensions(){
    	if(!scrollPane.isShowing()) {
    		return;
    	}
        int spWidth = (int) panel.getVisibleRect().getWidth();
        int spHeight = (int) panel.getVisibleRect().getHeight();
    	int spXOffset = (int) scrollPane.getViewport().getLocationOnScreen().getX() - (int) frame.getContentPane().getLocationOnScreen().getX();
    	int spYOffset = (int) scrollPane.getViewport().getLocationOnScreen().getY() - (int) frame.getContentPane().getLocationOnScreen().getY();
    }

Unfortunately, since the JScrollPane is a sub component and you can’t set the size directly (ie you use setPreferredSize), you will have to put this code in a change listener.


        scrollPane.getHorizontalScrollBar().addAdjustmentListener(new AdjustmentListener(){
			@Override
			public void adjustmentValueChanged(AdjustmentEvent arg0) {
				adjustToScrollPaneDimensions();
			}
        });
        canvasScrollPane.getViewport().addChangeListener(new ChangeListener(){
			@Override
			public void stateChanged(ChangeEvent e) {
				adjustToScrollPaneDimensions();
			}
        });