JMenuItem的单元测试

0 java junit swing jmenuitem actionevent

我是JUnit测试的新手.我正在尝试测试导出报告的方法.基本上,此方法会弹出一个保存菜单,以选择保存文件的位置,并从另一个类获取报告.我不确定我需要在这里测试什么或者如何测试它.我已经添加了我的JMenuItem和我的actionEvent.任何想法或帮助都会被大大接受.

这是我的JMenuItem:

    JMenuItem jMenuFileexportProjectReport = new JMenuItem(exportProjectReportAction); 
Run Code Online (Sandbox Code Playgroud)

这是我对JMenuItem的Action事件:

        public Action exportProjectReportAction =
            new AbstractAction(Local.getString("Export Project Report")) {

            public void actionPerformed(ActionEvent e) {
                reportExportAction(e);
            }
    };
Run Code Online (Sandbox Code Playgroud)

以下是导出报告的方法:

public void reportExportAction(ActionEvent e) {

            JFileChooser chooser = new JFileChooser();
            chooser.setFileHidingEnabled(false);
            chooser.setDialogTitle(Local.getString("Export Project Report"));
            chooser.setAcceptAllFileFilterUsed(false);
            chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
            chooser.addChoosableFileFilter(
                    new AllFilesFilter(AllFilesFilter.XHTML));
            chooser.addChoosableFileFilter(new AllFilesFilter(AllFilesFilter.HTML));

            String lastSel = (String) Context.get("LAST_SELECTED_EXPORT_FILE");
            if (lastSel != null) {
                    chooser.setCurrentDirectory(new File(lastSel));
            }

            ProjectExportDialog dlg =
                    new ProjectExportDialog(
                            App.getFrame(),
                            Local.getString("Export Project Report"),
                            chooser);

            String enc = (String) Context.get("EXPORT_FILE_ENCODING");
            if (enc != null) {
                    dlg.encCB.setSelectedItem(enc);
            }



            Dimension dlgSize = new Dimension(550, 500);
            dlg.setSize(dlgSize);
            Dimension frmSize = App.getFrame().getSize();
            Point loc = App.getFrame().getLocation();
            dlg.setLocation(
                    (frmSize.width - dlgSize.width) / 2 + loc.x,
                    (frmSize.height - dlgSize.height) / 2 + loc.y);
            dlg.setVisible(true);

            if (dlg.CANCELLED) {
                    return;
            }

                    Context.put(
                            "LAST_SELECTED_EXPORT_FILE",
                            chooser.getSelectedFile().getPath());

            int ei = dlg.encCB.getSelectedIndex();
            enc = null;
            if (ei == 1) {
                    enc = "UTF-8";
            }
            boolean nument = (ei == 2);
            File f = chooser.getSelectedFile();
            boolean xhtml =
                    chooser.getFileFilter().getDescription().indexOf("XHTML") > -1;
             CurrentProject.save();
             ReportExporter.export(CurrentProject.get(), chooser.getSelectedFile(), enc, xhtml, 
                              nument); 
            }
Run Code Online (Sandbox Code Playgroud)

创建HTML报告的类:

public class ReportExporter {

static boolean _chunked = false;
static boolean _num = false;
static boolean _xhtml = false;
static boolean _copyImages = false;
static File output = null;
static String _charset = null;
static boolean _titlesAsHeaders = false;
static boolean _navigation = false;

static String charsetString = "\n";




public static void export(Project prj, File f, String charset, boolean xhtml, boolean chunked) {


    _chunked = chunked;
    _charset = charset;
    _xhtml = xhtml;

    if (f.isDirectory()) {
        output = new File(f.getPath() + "/Project Report.html");
    }
    else {
        output = f;
    }

    NoteList nl = CurrentStorage.get().openNoteList(prj);
    Vector notes = (Vector) nl.getAllNotes(); 



   //Creates Labels for the HTML output for each section.
   String notesLabelHTML = "Notes";
   String tasksLabelHTML = "Tasks";
   String eventsLabHTML = "Events";


    //NotesVectorSorter.sort(notes);
    Collections.sort(notes);



    Writer fw;

    if (output.getName().indexOf(".htm") == -1) {
        String dir = output.getPath();
        String ext = ".html";

        String nfile = dir + ext;

        output = new File(nfile);
    }        
    try {
        if (charset != null) {
            fw = new OutputStreamWriter(new FileOutputStream(output),
                    charset);
            charsetString = "<meta http-equiv=\"Content-Type\" content=\"text/html; charset="
                    + charset + "\" />";
        }
        else
            fw = new FileWriter(output);
    }
    catch (Exception ex) {
        new ExceptionDialog(ex, "Failed to write to " + output, "");
        return;
    }

    //Writes the title and the notes section of the HTMl Report
    write(fw, "<html>\n<head>\n" + charsetString + "<title>"
            + prj.getTitle()
            + "</title>\n</head>\n<body>\n<h1 class=\"projecttitle\">" 
            + prj.getTitle() +  "</h1>\n" +"\n<br>\n" 
            + "</title>\n</head>\n<body>\n<h2 class=\"projecttitle\">"
            + notesLabelHTML + "</h2>\n" );
    generateChunks(fw, notes);

    //Writes the Task section of the HTML Report
    write(fw, "\n<hr></hr><a" +"</title>\n</head>\n<body>\n<h2 class=\"projecttitle\">" + "\n<br>\n"
            + tasksLabelHTML + "</h2>\n" );

    //writes the Events section of the HTML Report
    write(fw, "\n<hr></hr><a" +"</title>\n</head>\n<body>\n<h2 class=\"projecttitle\">" + "\n<br>\n"
            + eventsLabHTML + "</h2>\n" );

    //Writes the ending of the report with the data and time
    write(fw, "\n<hr></hr><a "
            + "\n<br></br>\n" + new Date().toString()
            + "\n</body>\n</html>");
    try {
        fw.flush();
        fw.close();
    }
    catch (Exception ex) {
        new ExceptionDialog(ex, "Failed to write to " + output, "");
    }
}




public static String getNoteHTML(Note note) {
    String text = "";
    StringWriter sw = new StringWriter();
    AltHTMLWriter writer = new AltHTMLWriter(sw,
            (HTMLDocument) CurrentStorage.get().openNote(note), _charset,
            _num);
    try {
        writer.write();
        sw.flush();
        sw.close();
    }
    catch (Exception ex) {
        new ExceptionDialog(ex);
    }
    text = sw.toString();
    if (_xhtml) {
        text = HTMLFileExport.convertToXHTML(text);
    }

    text = Pattern
            .compile("<body(.*?)>", java.util.regex.Pattern.DOTALL
                    + java.util.regex.Pattern.CASE_INSENSITIVE).split(text)[1];
    text = Pattern
            .compile("</body>", java.util.regex.Pattern.DOTALL
                    + java.util.regex.Pattern.CASE_INSENSITIVE).split(text)[0];

    text = "<div class=\"note\">" + text + "</div>";

    if (_titlesAsHeaders) {
                    text = "\n\n<div class=\"date\">"
                + note.getDate().getFullDateString()
                + ":</div>\n<h1 class=\"title\">" + note.getTitle()
                + "</h1>\n" + text;
    }

    return text;
}


private static String generateNav(Note prev, Note next) {
    String s = "<hr></hr><div class=\"navigation\"><table border=\"0\" width=\"100%\" cellpadding=\"2\"><tr><td width=\"33%\">";
    if (prev != null)  {  
        s += "<div class=\"navitem\"><a href=\"" + prev.getId() + ".html\">"
                + Local.getString("Previous") + "</a><br></br>"
                + prev.getDate().getMediumDateString() + " "
                + prev.getTitle() + "</div>";
    }

    else {
        s += "&nbsp;";
            s += "</td><td width=\"34%\" align=\"center\"><a href=\""
            + output.getName()
            + "\">Up</a></td><td width=\"33%\" align=\"right\">";
    }

    if (next != null) {
        s += "<div class=\"navitem\"><a href=\"" + next.getId() + ".html\">"
                + Local.getString("Next") + "</a><br></br>"
                + next.getDate().getMediumDateString() + " "
                + next.getTitle() + "</div>";
    }

    else {
        s += "&nbsp;";
    }
    s += "</td></tr></table></div>\n";
    return s;
}



private static void generateChunks(Writer w, Vector notes) {
    Object[] n = notes.toArray();
    for (int i = 0; i < n.length; i++) {
        Note note = (Note) n[i];
        CalendarDate d = note.getDate();
        if (_chunked) {
            File f = new File(output.getParentFile().getPath() + "/"
                    + note.getId()
                    + ".html");
            Writer fw = null;
            try {
                if (_charset != null) {
                    fw = new OutputStreamWriter(new FileOutputStream(f),
                            _charset);
                }

                else {
                    fw = new FileWriter(f);
                }

                String s = "<html>\n<head>\n"+charsetString+"<title>" + note.getTitle()
                        + "</title>\n</head>\n<body>\n" + getNoteHTML(note);

                if (_navigation) {
                    Note nprev = null;
                    if (i > 0) {
                        nprev = (Note) n[i - 1];
                    }

                    Note nnext = null;
                    if (i < n.length - 1) {
                        nnext = (Note) n[i + 1];
                    }

                    s += generateNav(nprev, nnext);
                }
                s += "\n</body>\n</html>";
                fw.write(s);
                fw.flush();
                fw.close();
            }
            catch (Exception ex) {
                new ExceptionDialog(ex, "Failed to write to " + output, "");
            }
        }
        else {
            write(w, "<a name=\""  + "\">" + note.getDate() +"</a>\n" + getNoteHTML(note) + "</a>\n");
        }
    }
}


private static void write(Writer w, String s) {
    try {
        w.write(s);
    }
    catch (Exception ex) {
        new ExceptionDialog(ex, "Failed to write to " + output, "");
    }
}
Run Code Online (Sandbox Code Playgroud)

ero*_*osb 5

正如已经提到的那样,基本上你应该从GUI代码移出应用程序逻辑.

单元测试通常不像写入工作代码那样工作,然后添加测试.首先你应该考虑一下

  • 什么是逻辑的可测试部分,什么不是
  • 你将如何分开他们
  • 你将如何测试可测试的部分

直接测试GUI代码通常不起作用(除了一些罕见且设计良好的UI框架),因为您的测试必须处理许多技术问题(如框架初始化,UI对象的实例化,正确触发事件等) .所以你应该创建一个更抽象的应用层.从更大的角度来看,它将导致众所周知的模型 - 视图 - 控制器模式(自Smalltalk以来用户界面的事实上的标准设计模式),其中模型层独立于UI框架,因此它是可测试的.

因此,作为某种案例研究,让我们来看看上面的概念:

首先让我们检查什么是可测试的,什么不是,以及什么阻止你编写测试:

chooser.setDialogTitle(Local.getString("Export Project Report"));

这条单线处理渲染和i18n,这不是一个好主意.此外,静态方法的使用是用于编写测试的顶级阻止反模式.

String lastSel =(String)Context.get("LAST_SELECTED_EXPORT_FILE");

静态方法再次调用此处,也很难确定Context类的责任.

Local.getString("导出项目报告")

也是一个静态调用,也是一种重复的代码

...等等(到处都是静态调用).现在让我们看看我们可以为此创建哪种更抽象的模型.首先让我们从需求的一些文字描述开始:

  • 有一个标题(在JFileChooser和ProjectExportDialog中使用)将通过关键的"导出项目报告"进行国际化
  • 有一个先前选择的目录(lastSel),我们在开始时授予了这个值
  • encoding(enc)也类似于enc,可空值
  • 对话框位置(定位)包含一些算术,我们应该测试一下
  • 如果用户选择了一个文件,那么我们应该将它存储为最后选择的目录
  • 还有一个开放的(当前)项目,我们将在最后保存
  • 有一些你称之为"nument"的东西,我不明白它是什么,但如果用户从dlg.encCB中选择第二个条目,它应该是真的

不稳定的部分:

  • chooser ...调用:JFileChooser的UI特定配置,也不包含任何控制结构或计算,所以我们不会测试它

现在我们将设计一个可测试的模型类.在这样做的同时,我们将牢记两个原则:

  • 我们将尽可能多地将逻辑放入模型中
  • 我们现在不想重写整个应用程序.因此,我们不是去除所有静态调用,而是尽可能简单地抽象它们.

所以现在让我们为此创建一些抽象模型(代码后的摘要):

public class ProjectExportModel {
  // see the reasoning below
  public static ProjectExportModel create() {
    return new ProjectExportModel(Local::getString,
        (String) Context.get("LAST_SELECTED_EXPORT_FILE"),
        Context::put);
  }

  private final Function<String, String> i18n;

  private final File lastSelectedExportFile;

  private final Consumer<File> lastSelectedFileSaver;

  private String encoding;

  private boolean nument;

  private boolean xhtml;

  public ProjectExportModel(final Function<String, String> i18n, final File lastSelectedExportFile,
      final Consumer<File> lastSelectedFileSaver) {
    this.i18n = i18n;
    this.lastSelectedExportFile = lastSelectedExportFile;
    this.lastSelectedFileSaver = lastSelectedFileSaver;
  }

  /**
   * Called after a file has been selected from the JFileChooser
   *
   * Things to test:
   * - lastSelectedFileSaver.accept(file.getPath()) should be called - you may use a
   * mocking library to test
   * - the xhtml flag should be changed - testing is easy
   *
   */
  public void fileSelected(final File file) {
    // TODO
  }

  /**
   * At this point we break a bit the concept of the UI-independent model layer, since Point and Dimension
   * are UI-framework-related classes. But these 2 classes are easy to instantiate and easy to assert on the
   * returned value, so good-enough solution this time.
   */
  public Point getDialogLocation(final Dimension frameSize, final Point frameLocation) {
    return null; // TODO implement the positioning
  }

  public String getFrameTitle() {
    // TODO test if it calls and returns i18n.get("Export Project Model") - you need mocking here too
    return null;
  }

  /**
   * Two things to be tested here:
   * - if CurrentProject.save() is called
   * - if ReportExporter.export(...) is called with the right parameters
   *
   * You are quite stuck here, since static methods cannot be mocked. Instead it would be better to change your APIs to make
   * these instance methods, since in the current way it is untestable. After changing these to instance methods, you should add
   * 2 more parameters to the constructor: a Project instance and a ReportExporter instance.
   * You can use mockito or easymock for mocking.
   */
  public void save() {

  }

  /**
   * You may call it from the view layer after calling fileSelected().
   *
   * To be tested:
   * - the proper change of the encoding member
   * - the proper change of the nument member
   */
  public void selectedEncodingChanged(final int selectedIndex) {
    // TODO implemenent the change of encoding and nument member
  }

}
Run Code Online (Sandbox Code Playgroud)

摘要:

  • 这个类很容易实例化和测试
  • 在测试中,您将使用其显式构造函数来创建实例
  • 对于"生产"用法,您必须创建一个View类,它处理与swing相关的代码,接受ProjectExportModel实例作为其参数,并调用它的方法,因此您将测试的模型连接到不可测试的UI相关代码,同时保持后者最小化.此外,在这种情况下,您将使用ProjectExportModel.create()创建模型实例,因为该方法以一种与应用程序的其他静态方法或多或少良好交互的方式连接其他依赖项.这是一个很好的技术,用于提取可测试的部分,而您不必从应用程序中删除所有静态方法,我们只是将它们分开.