如何对Java主方法进行单元测试?

5
我需要一个类通过其主方法传递2个参数,如果传递少于2个参数,则显示系统错误消息。这里我为主要方法编写了一个单元测试,在运行测试时,它在“运行”处停止(既不显示通过也不显示失败)。请提供建议。
Example.java
public class Example 
{
    private static String str1   = null;
    private static String str2   = null;

    public static void main(String[] args)
    {
        if( args.length != 2 )
        {
            call();
        }

        Example ex = new Example(args[0], args[1]);
        ex.getData();
    }

    public Example(String str1, String str2)
    {
        Example.str1 = str1;
        Example.str2 = str2;
    }

    public void getData(){
        System.out.println("Name is: "+str1);
        System.out.println("City is: "+str2);
    }

    private static void call()
    {
        System.err.println("Usage: String1 String2");
        System.err.println("Where: ");
        System.err.println("       String1 - Name");
        System.err.println("       String1 - City");
        System.exit(1);
    }   
}

ExampleTest.java

public class ExampleTest {
    @Test
    public void testPassingWrongNumberOfInputs() {
        StringBuffer sb = new StringBuffer();
        sb.append("Usage: String1 String2")
        .append("Where: ")
        .append("       String1 - Name")
        .append("       String1 - City");

        String expectedErrorMessage = sb.toString();

        ByteArrayOutputStream outContent = new ByteArrayOutputStream();
        System.setErr(new PrintStream(outContent));
        String[] args = {"one"};
        Example.main(args);

        assertEquals(expectedErrorMessage, outContent.toString());
    }
}

2
什么是目的?主方法相当不单元化。100%覆盖率的想法相当有问题:它往往会使测试变得更加脆弱,因为人们开始发明无意义的测试,只是为了达到100%的覆盖率。 - Willem Van Onsem
1
你的测试有点有趣(并非恶意)。它说:请测试这个方法,它将会终止你正在运行的环境,然后我们就可以看看,你是否足够强壮,能够告诉我这个测试是否好用。换句话说:重新检查一下 System.exit(1); 的作用。 - Tom
2
System.exit() 的调用可能与您的代码停止运行有关... - Oliver Charlesworth
1
我们公司的编码审查团队要求我为主方法添加单元测试。 - devlperMoose
4个回答

2
以下内容怎么样:

如下所示:

class TestingSecurityManager extends SecurityManager {
  @Override public void checkExit(int status) {
    throw new SecurityException();
  }
}

Then in your test...

public class ExampleTest {
    @Test
    public void testPassingWrongNumberOfInputs() {
        StringBuffer sb = new StringBuffer();
        sb.append("Usage: String1 String2")
        .append("Where: ")
        .append("       String1 - Name")
        .append("       String1 - City");

        String expectedErrorMessage = sb.toString();

        ByteArrayOutputStream outContent = new ByteArrayOutputStream();
        System.setErr(new PrintStream(outContent));
        String[] args = {"one"};

        TestSecurityManager sm = new TestSecurityManager ();
        System.setSecurityManager(sm);

        try {
            Example.main(args);
            //should throw
            fail("Should have thrown exception");
        } catch (SecurityException se) {
        }

        assertEquals(expectedErrorMessage, outContent.toString());
    }
}

1
为什么不像new SecurityException(Integer.toString(status))一样将状态传递到异常中,然后在catch子句中使用assertEquals("1", se.getMessage())?也许你应该将setSecurityManager代码移动到设置方法中,并创建一个拆卸方法来恢复安全管理器。相同的VM可能会用于多个测试。这非常好。 - emory
好的想法。一切皆有可能。 - Jose Martinez
@JoseMartinez 我尝试了你的代码,但是测试失败并出现以下错误:org.junit.ComparisonFailure: expected:<[Usage: String1 String2Where: String1 - Name String1 - City]> but was:<[access: access allowed ("java.io.FilePermission" "C:\codebase\jboss\TestProject\bin\com\usaa\example\Example.class" "read") access: access allowed ("java.io.FilePermission" "C:\codebase\jboss\TestProject\bin\com\usaa\example\Example.class" "read") Usage: String1 String2 Where: String1 - Name String1 - City ]> - devlperMoose
@JoseMartinez 我找到了我收到访问允许消息的地方。这是因为我添加了 -Djava.security.debug=access,failure 到 VM args。我已经将它们删除,但仍然收到比较失败错误。请建议。org.junit.ComparisonFailure: 期望值:<...age: String1 String2[ Where: String1 - Name String2 - City]> 但实际值为:<...age: String1 String2[ Where: String1 - Name String2 - City ]> - devlperMoose
你在第二个“City”后面多了一个空格。在文本编辑器中将一行复制到另一行下面,这样你就可以轻松地进行比较了。 - GregT

2

我终于能够按照以下所示编写单元测试。我只测试了方法是否触发了System.exit(1)代码。

public class ExampleTest {
    private SecurityManager m;
    private TestSecurityManager sm;

    @Before
    public void setUp() 
    {
        m = System.getSecurityManager();
        sm = new TestSecurityManager ();
        System.setSecurityManager(sm);
    }

    @After
    public void tearDown()
    {   
        System.setSecurityManager(m);
    }

    @Test
    public void testPassingWrongNumberOfInputs() {
        try {
            Example.main(new String[] {"one"});
        } catch (SecurityException se) {
            assertEquals("1", se.getMessage());
        }
    }
}

class TestSecurityManager extends SecurityManager {
    @Override 
    public void checkPermission(Permission permission) {            
        if ("exitVM".equals(permission.getName())) 
        {
            throw new SecurityException("System.exit attempted and blocked.");
        }
    }
    @Override 
    public void checkExit(int status) {
        throw new SecurityException(Integer.toString(status));
    }
}

0

移除System.exit(1)调用,你不需要它。你的应用程序将在main()完成后退出,而不需要显式终止VM的不必要调用。这个调用很可能会导致你的JUnit在到达assertEquals语句之前停止执行,因为你刚刚告诉VM退出了。


然后你建议他删除 System.exit(1),接着你也应该告诉他如何避免他现在可能会遇到的 ArrayIndexOutOfBoundsException - Tom
1
@Tom 修复 ArrayOutOfBoundsException 应该很容易。然而,高级用户倾向于使用退出代码来指示程序的状态。如果您没有明确将其设置为1,则它将为0 - 错误地表示成功。 - emory
1
无论“高级用户”是否返回退出代码都不是问题。调用System.exit()会导致VM过早退出。您的测试代码和JUnit在同一个VM上执行。如果您的测试代码退出了VM,那么您的JUnit也会退出。这从来都不是一个好习惯,因为如果此代码在应用服务器上执行,则刚刚将应用服务器关闭。这在生产中永远不是一件好事。 - Kevin Hooke
如果你正在寻找一种从方法中返回结果的机制,那么请更改你的方法签名以指定返回类型,并使用 return 语句返回一个值(这可能是你正在寻找的 - System.exit() 绝对不是你要寻找的方式)。 - Kevin Hooke
我需要使用System.exit(1),因为这个Example类将会在生产环境的脚本中被调用。脚本无法理解"return;"语句的含义。 - devlperMoose
显示剩余2条评论

0
重命名主方法,并添加返回值,以便您可以测试它。从主方法中调用这个新方法。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接