Forum Discussion

Novari-QA's avatar
Novari-QA
Frequent Contributor
8 years ago

Call function from string value JavaScript

I was hoping that TestComplete offered a method in which we can dynamically call specific functions based off of a string variable.

 

For example: This is how I would do it in C#. Unfortunately I am not sure if JavaScript has something similar, or even if TestComplete offers it.

 

using System;
using System.Reflection;

/// <summary>
///     Will invoke the method passed in. Then, it will return the generic type used in the function
///     Example: InvokeMethod<int>("methodName", "param");
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="methodName"></param>
/// <param name="stringParam"></param>
/// <returns></returns>
public static T InvokeMethod<T>(string methodName, string stringParam)
{
        return (T)typeof(T).InvokeMember(methodName, BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Static,
                                              null, null, new Object[] { stringParam });
}

 

 

Thoughts?

  • Different script languages have a functionality like "Evaluate" or "eval" which will take any string and evaluate it as such.  It works well but it does run some risks.

     

    There is a method of Runner.CallMethod (https://support.smartbear.com/testcomplete/docs/reference/project-objects/items/script/runner/callmethod.html). It's listed as obselete because it had been the means, before, of calling methods from other script units.  That's no longer necessary.  But for doing the kind of data reflection you're wanting to do, this still works well.

     

    If your method to be invoked is a method on an object, you can use aqObject.CallMethod instead (https://support.smartbear.com/testcomplete/docs/reference/program-objects/aqobject/callmethod.html).

     

    I created a kind of data reflection myself as part of the mini-framework I have under development.  The code is below.  It requires a certain structuring for creating the classes themself (each class is a script unit named for the class with a method called "returnClass" which returns an instance of the object. I use the flexibility of the Runner.CallMethod means of doing this which works well in my situation:

    /* Initial source retrieved from https://bitbucket.org/tristaanogre/tabledrivenframework and developed by Robert Martin. 
    The initial source is under a Creative Commons Creative Commons Attribution-ShareAlike 4.0 International License and any 
    modifications to this source are similarly available under the same license */
    
    function toProperties(listProperties) {
    
        var listLength,
        listPropertyName,
        listPropertyValue,
        listPropertyString;
        listLength = aqString.GetListLength(listProperties);
        listPropertyString = '';
        for (var i = 0; i<listLength; i += 2){
            listPropertyName = aqString.GetListItem(listProperties, i);
            listPropertyValue = aqString.GetListItem(listProperties, i+1);
            listPropertyString = listPropertyString + '"' + listPropertyName + '": "' + listPropertyValue + '", ';
        }
        return JSON.parse('{' + aqString.SubString(listPropertyString, 0, aqString.GetLength(listPropertyString)-2) + '}');
    
    }
    
    function returnClass(className, properties){
        var classMethod = className + ".returnClass";
        var localObject = Runner.CallMethod(classMethod)
        if (properties === undefined) {
            properties = "";
        }
        localObject.data = toProperties(properties);
        return localObject;
    }
  • Hiya,

     

    Normally - there are better ways to avoid this entirely. I'd say re-evaluate your design and see if eval is really the only way to go

     

    For example you can export your function and call it in another file.

     

    EDIT: I noticed you had params. Edited my solution to include params too. 

     

    // file1

     

    function foo(str) {

        console.log('foo' + str)
    }

     

    function bar(str, str2) {

        console.log('bar' + str + str2)

    }

     

    module.exports = { foo, bar } 

     

    // file2

    const str = 'foo'

    function runner(methodName, paramsArr) {

         return require('file1')[methodName](...params)  //use es6 spread operator to cater to any num of params

    }

     

    • tristaanogre's avatar
      tristaanogre
      Esteemed Contributor

      KSQian That is a neat trick.  The framework that I posted the code from up thread has the Runner.CallMethod embedded in a script extension... which is JScript which is es3, not 6, so unfortunately I can't use that there...  But it is something I'm going to tuck away in my bag of tricks for future reference.

      • KSQian's avatar
        KSQian
        Contributor

        Gotcha Robert!

         

        es3 has apply which is another way of doing the same thing:

         

        // file2

        const str = 'foo'

        function runner(methodName, paramsArr) {

             return require('file1')[methodName].apply(this, paramsArr) 

        }

         

         

  • tristaanogre's avatar
    tristaanogre
    Esteemed Contributor

    Different script languages have a functionality like "Evaluate" or "eval" which will take any string and evaluate it as such.  It works well but it does run some risks.

     

    There is a method of Runner.CallMethod (https://support.smartbear.com/testcomplete/docs/reference/project-objects/items/script/runner/callmethod.html). It's listed as obselete because it had been the means, before, of calling methods from other script units.  That's no longer necessary.  But for doing the kind of data reflection you're wanting to do, this still works well.

     

    If your method to be invoked is a method on an object, you can use aqObject.CallMethod instead (https://support.smartbear.com/testcomplete/docs/reference/program-objects/aqobject/callmethod.html).

     

    I created a kind of data reflection myself as part of the mini-framework I have under development.  The code is below.  It requires a certain structuring for creating the classes themself (each class is a script unit named for the class with a method called "returnClass" which returns an instance of the object. I use the flexibility of the Runner.CallMethod means of doing this which works well in my situation:

    /* Initial source retrieved from https://bitbucket.org/tristaanogre/tabledrivenframework and developed by Robert Martin. 
    The initial source is under a Creative Commons Creative Commons Attribution-ShareAlike 4.0 International License and any 
    modifications to this source are similarly available under the same license */
    
    function toProperties(listProperties) {
    
        var listLength,
        listPropertyName,
        listPropertyValue,
        listPropertyString;
        listLength = aqString.GetListLength(listProperties);
        listPropertyString = '';
        for (var i = 0; i<listLength; i += 2){
            listPropertyName = aqString.GetListItem(listProperties, i);
            listPropertyValue = aqString.GetListItem(listProperties, i+1);
            listPropertyString = listPropertyString + '"' + listPropertyName + '": "' + listPropertyValue + '", ';
        }
        return JSON.parse('{' + aqString.SubString(listPropertyString, 0, aqString.GetLength(listPropertyString)-2) + '}');
    
    }
    
    function returnClass(className, properties){
        var classMethod = className + ".returnClass";
        var localObject = Runner.CallMethod(classMethod)
        if (properties === undefined) {
            properties = "";
        }
        localObject.data = toProperties(properties);
        return localObject;
    }