Friday, 17 March 2017

Mistakes using Java main and examples of coding without main

TL;DR A potentially contentious post where I describe how I've survived without writing a lot of Java main methods, and how learning from code that is often driven by a main method has not helped some people. I do not argue for not learning how to write main methods. I do not argue against main methods. I argue for learning them later, after you know how to code Java. I argue for learning how to use test runners and built in features of maven or other build tools to execute your @Test code.




Update - 17/March/2017 - for some reason this was sitting in draft for over two years (last date was 3/March/2015). I seem to have used some of the material in the post in other posts (possibly I thought this post was too large), but there are some examples in here I haven't mentioned before so I hereby click publish and release it to the world.

I think I've written a 'main' method only 4 or 5 times in my Java career.

I rarely have to package up my code in a form that can run from the command line.

I have only recently (in January 2015) written a Java GUI, which starts from a main method.

All of the rest of my code, and I've written a lot of Java code, I have executed as a JUnit or TestNG (in the early days) @Test method.

I only need a main method when writing an application.

I normally write code to automate applications.

Most of the code I write takes the form of automated checks of an application to confirm assumptions represented as assertions in the code.

Most people learning Java for the first time do not learn to do write code in the form of @Test methods, they learn the main method first. And then always use a main method.

Here are some of the mistakes I've seen people writing automation code make with the main method.

Some people will read this post and think - uh oh, he's talking about me. And I probably am for one of the examples. But I haven't seen just one person making these mistakes, I've seen many people make the following mistakes.

And I have included some examples of how I have managed to go through my Java career and avoid packaging my code into an app.

Mistake - write all code in a main method

Since people learn 'main' first, they often write all their code in, or from, a main method.

Leading to:
  • hardly any classes
  • hardly any abstractions
  • harder to unit test because no-one really trains you to write tests around a main method
  • very often very few unit tests in their code base because they started with a main method, rather than @Test methods
  • lots of copy pasted code across lots of projects

Unfortunately, I've also encountered situations where people are taught to write automation code like this. With no mention of a TestRunner (e.g. JUnit or TestNG). So effectively they are having to learn Java at the same time as writing a Test Execution engine, something that you get for free with JUnit or TestNG.

No wonder people give up with Java and jump to a scripting language instead.

I find it better to start with @Test annotated methods, and build up the code base, and work towards a main method (if I need it), but not to start with a main method.

Mistake - multiple main methods

I can see advantages in having multiple entry points into your packaged code. It might be a good idea in certain situations, and we know we can define which specific main method to run when we start the jar file.

But sometimes main methods are added because people don't know why they needed them in the first place.

I've seen code where
  • main methods don't do anything, because doesn't every class need a main method?

Why?

Because every example they have seen, starts with a main method, and most of the example code people see in books is not related, therefore each chapter has a separate application, i.e. a separate main method.

I avoid this problem by driving everything with @Test methods - even my main methods will have an @Test around them. And I can run the disparate parts of my code base using the filters provided by JUnit Suites or maven configuration, or running individual @Test methods or classes from the IDE.

Advice and Examples

I advise you learn to package your code, and use main methods, at the point when you are writing an application for other people to use. But not before.

Because you may not need to.

When you are writing @Test methods you can use the standard configuration of maven to run all your JUnit tests automatically without you having to create your own test runner or main method.

You can also write @Test methods that you can run from the IDE to gain many of the benefits of scripting languages.

Example - Selenium Simplified Book Generation

When I wrote Selenium Simplified. I couldn't find a text file based book generator that I liked, given the use cases I needed.

So I wrote my own.

It was a fairly large Java project, with a lot of classes.

And no main method.

The IDE was my execution GUI.

     @Test
     public void writeOutputToAllDestinations() throws SAXException, IOException{
   
          // http://www.saxproject.org/quickstart.html
         
          String evilMarkupFilePath = "C:\\Users\\alan\\Documents\\selenium_simplified";
          evilMarkupFilePath += "\\_metadata\\";
          String evilMarkupFileName = "seleniumSimplified";

          String xmlConfigFileName = evilMarkupFilePath + evilMarkupFileName + ".xml";

          EvilTesterMarkupSAX handler = new EvilTesterMarkupSAX();    
          handler.processThisConfigurationFile(xmlConfigFileName);

          ETM_Output outputDetails = handler.getOutput();
          outputDetails.outputToAllDestinations(handler, evilMarkupFilePath);        
     }


  • If I wanted to process a different book, I changed the file path.
  • If I wanted different options, I amended the outputDetails
  • I had multiple @Test methods for different configurations with relevant names

I built my book by right clicking on the method and selecting "Run" as JUnit Test

This code project still doesn't have a main method.

Example - Code for Java For Testers

After some copy paste and proofing errors during the creation of Selenium Simplified, I wasn't prepared to write another book with code in it, until I could pull code from the main source into the book.

I couldn't find a tool that worked the way I wanted, so I wrote code to automate the process for me.

Looking through my history I can see that on revision 14, I added the following code:

public class MainTest {
    @Ignore("only use this for debugging, it is not part of the build")
    @Test
    public void debugMain() throws IOException {
        String []args = {"D:/JavaForTesters/tools/javaForTesters.properties"};
        ApplicationRunner.main(args);
    }
}

I created a new class called ApplicationRunner, with a main method. But I was at this point still running the code from the IDE, under control of a @Test, which  only ran from the IDE because it was @Ignore annotated, so would not run from CI.

It was not until revision 16, that I added the manifest details into pom.xml to allow me to package the code as an application.

When I did this in revision 16, I had the ability to run the code outside the IDE, but I retained the ability from revision 14, to run it from the IDE by wrapping it in an @Test annotated method.

I used the @Test execution approach first.

Example - Backup MindMeister

I wrote some automation code to help me backup my MindMeister Mind Maps

    @Test
    public void backupAllMaps(){
        MindMeister mm = new MindMeister(myAPIKey, myToken);

        Map<String, String> folderPaths = mm.getFolderPaths();

        MindMeisterMap[] maps = mm.getAllMaps();

        for(MindMeisterMap map : maps){

            MapExportDetails exportDetails = mm.getMapExportDetails(map.id);
            String thePath = "";
            try{
                if(folderPaths.containsKey(map.folderId)){
                    thePath = folderPaths.get(map.folderId);
                }
            }catch(Exception e){
                thePath = "";
            }

            mm.getFileFrom(exportDetails.mindmeister, thePath);
        }
    }

I run this automation code by right clicking on the 'backupAllMaps' @Test method in the IDE.

I had multiple automation use cases. All executed by right clicking on an appropriately named @Test method, and running as JUnit test.

This is the kind of use case people put forward as a reason for using scripting languages.

Summary

I actually write a lot of my ad-hoc support code this way. By running it from the IDE as an @Test method.

I have many other examples of this approach on my disk but I think the above are representative enough.

This approach gives me many of the benefits that people claim that scripting language bring to their work-flow.

Only when I need to run the code during a work-flow outside the IDE or allow other people to run the code, do I use a main method.

And at that point, I generally make sure I have an @Test method which can all the main method to allow me to create ad-hoc debug test runs.

It is important to learn how to use main, but not as one of the first things you do. Particularly when you are writing automation code as opposed to application code.

The point at which your automation code becomes application code, is the point at which you learn how to package your code and use the main method.

4 comments:

  1. Thanks for Your examples. I love your book and I'm now wring through it. As a starting tester how writes test automation code I find it hard to start from tests and then work to the classes and method. Im missing the chapter of bringing all the classes together and make it run. In tor book it stays isolated. With a main method it feels easier to understand and divide the logic.

    ReplyDelete
    Replies
    1. Hi Mark, Thanks for leaving a comment.

      I guess it depends what you want to bring all the classes together to do?

      If you are writing tests to automatically execute an app, then the tests do stay separate and you run them all with 'mvn test'.

      If you are trying to write an application then you gradually build up the functionality using test code, and you might even create a high level 'application' object that you could trigger with a single test (if you wanted). That was the basic approach I took when I wrote my pandocifier application (https://github.com/eviltester/pandocifier)

      I generally write the code that I Want to see, and write it in the @Test method so that the @Test method looks the way I Want it to, and then I go off and implement it.

      @Test is where we model the execution flow and assertions. The classes and methods are where we make it happen.

      It might be useful if you look through github at other applications/test projects etc. and see what other people do. Or contact me via the 'contact me' link form so I can reply iteratively as we explore what you are finding hard.

      Delete
  2. Thanks Alan, Informative.. Thumps Up!!

    ReplyDelete